🚧 wip: chekkupointo, basic functionality completed

This commit is contained in:
2026-02-26 03:12:14 +01:00
parent b13b3934f8
commit 90311d9832
21 changed files with 443 additions and 96 deletions
+2
View File
@@ -7,6 +7,8 @@ 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
Palette/domcolor.hpp Palette/domcolor.cpp
Palette/matchcolor.hpp Palette/matchcolor.cpp
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
+4 -2
View File
@@ -28,8 +28,8 @@
// action.previewDebounceTime number 300 Debounce time for preview action in milliseconds // 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.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.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.onSelected string "" Command to execute on confirmation
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path) // 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[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
@@ -37,6 +37,7 @@
// 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 "cmd" 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.restoreOnCancel boolean true Whether to run the restore command after cancel without confirming a wallpaper
// //
// style.image_width number 320 Width of each image // style.image_width number 320 Width of each image
// style.image_height number 200 Height of each image // style.image_height number 200 Height of each image
@@ -103,6 +104,7 @@ struct ActionConfigItems {
bool printSelected = true; bool printSelected = true;
bool printPreview = false; bool printPreview = false;
bool quitOnSelected = false; bool quitOnSelected = false;
bool restoreOnCancel = true;
}; };
struct StyleConfigItems { struct StyleConfigItems {
+8 -1
View File
@@ -230,6 +230,12 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
m_actionConfig.quitOnSelected = val.toBool(); m_actionConfig.quitOnSelected = val.toBool();
} }
} }
if (config.contains("restoreOnCancel")) {
const auto& val = config["restoreOnCancel"];
if (val.isBool()) {
m_actionConfig.restoreOnCancel = val.toBool();
}
}
} }
void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root) { void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root) {
@@ -405,6 +411,7 @@ void WallReel::Core::Config::Manager::captureState() {
process->disconnect(); process->disconnect();
QString result = success ? output : defaultVal; QString result = success ? output : defaultVal;
Logger::debug(QString("Capture result for key '%1': %2 (success: %3)").arg(key).arg(result).arg(success));
if (result.isEmpty()) result = defaultVal; if (result.isEmpty()) result = defaultVal;
_onCaptureResult(key, result); _onCaptureResult(key, result);
@@ -444,7 +451,7 @@ void WallReel::Core::Config::Manager::captureState() {
if (timer) { if (timer) {
timer->start(); timer->start();
} }
process->startCommand(item.cmd); process->start("sh", QStringList() << "-c" << item.cmd);
} }
} }
+5 -1
View File
@@ -2,7 +2,8 @@
#include <QImageReader> #include <QImageReader>
#include "../logger.hpp" #include "Palette/domcolor.hpp"
#include "logger.hpp"
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString& path, const QSize& size) { WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString& path, const QSize& size) {
Data* ret = new Data(path, size); Data* ret = new Data(path, size);
@@ -59,4 +60,7 @@ WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize)
// Create ID // Create ID
m_id = QString::number(qHash(m_file.absoluteFilePath())); m_id = QString::number(qHash(m_file.absoluteFilePath()));
// Get dominant color
m_dominantColor = Palette::getDominantColor(m_image);
} }
+14 -1
View File
@@ -10,6 +10,8 @@ class Data {
QString m_id; QString m_id;
QFileInfo m_file; QFileInfo m_file;
QImage m_image; QImage m_image;
QColor m_dominantColor;
QHash<QString, QString> m_colorCache;
Data(const QString& path, const QSize& size); Data(const QString& path, const QSize& size);
@@ -32,7 +34,18 @@ class Data {
const QFileInfo& getFileInfo() const { return m_file; } const QFileInfo& getFileInfo() const { return m_file; }
private: const QColor& getDominantColor() const { return m_dominantColor; }
std::optional<QString> getCachedColor(const QString& paletteName) const {
if (m_colorCache.contains(paletteName)) {
return m_colorCache.value(paletteName);
}
return std::nullopt;
}
void cacheColor(const QString& paletteName, const QString& colorName) {
m_colorCache.insert(paletteName, colorName);
}
}; };
} // namespace WallReel::Core::Image } // namespace WallReel::Core::Image
+53 -29
View File
@@ -29,14 +29,13 @@ WallReel::Core::Image::Model::Model(
emit progressChanged(); emit progressChanged();
}); });
// Update state when sort method changes // Pipeline: sort -> filter -> update properties
connect(this, &Model::currentSortTypeChanged, this, &Model::_onSortMethodChanged); connect(this, &Model::currentSortTypeChanged, this, &Model::_onSortMethodChanged);
connect(this, &Model::currentSortReverseChanged, this, &Model::_onSortMethodChanged); connect(this, &Model::currentSortReverseChanged, this, &Model::_onSortMethodChanged);
connect(this, &Model::searchTextChanged, this, &Model::_onSearchTextChanged);
connect(this, &Model::focusedImageChanged, this, &Model::_updateFocusedProperties);
m_sortIndices.resize(4); // None, Name, Date, Size m_sortIndices.resize(4); // None, Name, Date, Size
// Search text
connect(this, &Model::searchTextChanged, this, &Model::_onSearchTextChanged);
} }
WallReel::Core::Image::Model::~Model() { WallReel::Core::Image::Model::~Model() {
@@ -58,7 +57,7 @@ QVariant WallReel::Core::Image::Model::data(const QModelIndex& index, int role)
return QVariant(); return QVariant();
} }
int actualIndex = fromProxyIndex(index.row()); int actualIndex = _convertProxyIndex(index.row());
if (actualIndex < 0 || actualIndex >= m_data.count()) { if (actualIndex < 0 || actualIndex >= m_data.count()) {
Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); Logger::debug("Actual index out of bounds: " + QString::number(actualIndex));
return QVariant(); return QVariant();
@@ -126,12 +125,12 @@ void WallReel::Core::Image::Model::setSearchText(const QString& text) {
} }
} }
const WallReel::Core::Image::Data* WallReel::Core::Image::Model::getDataPtrAt(int index) const { WallReel::Core::Image::Data* WallReel::Core::Image::Model::imageAt(int index) {
if (index < 0 || index >= m_filteredIndices.count()) { if (index < 0 || index >= m_filteredIndices.count()) {
Logger::debug("Invalid index requested: " + QString::number(index)); Logger::debug("Invalid index requested: " + QString::number(index));
return nullptr; return nullptr;
} }
int actualIndex = fromProxyIndex(index); int actualIndex = _convertProxyIndex(index);
if (actualIndex < 0 || actualIndex >= m_data.count()) { if (actualIndex < 0 || actualIndex >= m_data.count()) {
Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); Logger::debug("Actual index out of bounds: " + QString::number(actualIndex));
return nullptr; return nullptr;
@@ -139,13 +138,17 @@ const WallReel::Core::Image::Data* WallReel::Core::Image::Model::getDataPtrAt(in
return m_data[actualIndex]; return m_data[actualIndex];
} }
WallReel::Core::Image::Data* WallReel::Core::Image::Model::focusedImage() {
return imageAt(m_focusedIndex);
}
QVariant WallReel::Core::Image::Model::dataAt(int index, const QString& roleName) const { QVariant WallReel::Core::Image::Model::dataAt(int index, const QString& roleName) const {
if (index < 0 || index >= m_filteredIndices.count()) { if (index < 0 || index >= m_filteredIndices.count()) {
Logger::debug("Invalid index requested: " + QString::number(index)); Logger::debug("Invalid index requested: " + QString::number(index));
return QVariant(); return QVariant();
} }
int actualIndex = fromProxyIndex(index); int actualIndex = _convertProxyIndex(index);
if (actualIndex < 0 || actualIndex >= m_data.count()) { if (actualIndex < 0 || actualIndex >= m_data.count()) {
Logger::debug("Actual index out of bounds: " + QString::number(actualIndex)); Logger::debug("Actual index out of bounds: " + QString::number(actualIndex));
return QVariant(); return QVariant();
@@ -185,26 +188,21 @@ void WallReel::Core::Image::Model::loadAndProcess(const QStringList& paths) {
emit totalCountChanged(); emit totalCountChanged();
} }
int WallReel::Core::Image::Model::fromProxyIndex(int proxyIndex) const {
if (proxyIndex < 0 || proxyIndex >= m_filteredIndices.size()) {
Logger::debug("Invalid proxy index requested: " + QString::number(proxyIndex));
return -1;
}
return m_filteredIndices[m_currentSortReverse ? (m_filteredIndices.size() - 1 - proxyIndex) : proxyIndex];
}
void WallReel::Core::Image::Model::focusOnIndex(int index) { void WallReel::Core::Image::Model::focusOnIndex(int index) {
if (index < 0 || index >= m_filteredIndices.count()) { if (index < 0 || index >= m_filteredIndices.count()) {
Logger::debug("Invalid index to focus on: " + QString::number(index)); Logger::debug("Invalid index to focus on: " + QString::number(index));
return; return;
} }
int actualIndex = fromProxyIndex(index); int actualIndex = _convertProxyIndex(index);
if (actualIndex < 0 || actualIndex >= m_data.count()) { if (actualIndex < 0 || actualIndex >= m_data.count()) {
Logger::debug("Actual index out of bounds for focus: " + QString::number(actualIndex)); Logger::debug("Actual index out of bounds for focus: " + QString::number(actualIndex));
return; return;
} }
if (m_focusedIndex != index) {
m_focusedIndex = index; m_focusedIndex = index;
_updateFocusedName(); emit focusedImageChanged();
_updateFocusedProperties();
}
} }
void WallReel::Core::Image::Model::stop() { void WallReel::Core::Image::Model::stop() {
@@ -216,6 +214,14 @@ void WallReel::Core::Image::Model::stop() {
} }
} }
int WallReel::Core::Image::Model::_convertProxyIndex(int proxyIndex) const {
if (proxyIndex < 0 || proxyIndex >= m_filteredIndices.size()) {
Logger::debug("Invalid proxy index requested: " + QString::number(proxyIndex));
return -1;
}
return m_filteredIndices[proxyIndex];
}
void WallReel::Core::Image::Model::_clearData() { void WallReel::Core::Image::Model::_clearData() {
beginResetModel(); beginResetModel();
m_provider.clear(); m_provider.clear();
@@ -257,11 +263,11 @@ void WallReel::Core::Image::Model::_updateSortIndices(Config::SortType type) {
std::sort(indices.begin(), indices.end(), compareFunc); std::sort(indices.begin(), indices.end(), compareFunc);
} }
void WallReel::Core::Image::Model::_updateFocusedName() { void WallReel::Core::Image::Model::_updateFocusedProperties() {
if (m_focusedIndex < 0 || m_focusedIndex >= m_data.count()) { if (m_focusedIndex < 0 || m_focusedIndex >= m_filteredIndices.size()) {
m_focusedName = ""; m_focusedName = "";
} else { } else {
int actualIndex = fromProxyIndex(m_focusedIndex); int actualIndex = _convertProxyIndex(m_focusedIndex);
if (actualIndex < 0 || actualIndex >= m_data.count()) { if (actualIndex < 0 || actualIndex >= m_data.count()) {
m_focusedName = ""; m_focusedName = "";
} else { } else {
@@ -272,18 +278,36 @@ void WallReel::Core::Image::Model::_updateFocusedName() {
emit focusedNameChanged(); emit focusedNameChanged();
} }
void WallReel::Core::Image::Model::_applySearchFilter() { void WallReel::Core::Image::Model::_applySearchFilter(bool informView) {
emit layoutAboutToBeChanged();
m_filteredIndices.clear();
const auto& sortedIndices = m_sortIndices[static_cast<int>(m_currentSortType)]; const auto& sortedIndices = m_sortIndices[static_cast<int>(m_currentSortType)];
for (int i = 0; i < sortedIndices.size(); ++i) { int srcPos = 0, resPos = 0;
int actualIndex = sortedIndices[i]; for (; srcPos < sortedIndices.size(); ++srcPos) {
int actualIndex = m_currentSortReverse ? sortedIndices[sortedIndices.size() - 1 - srcPos] : sortedIndices[srcPos];
const auto& item = m_data[actualIndex];
if (item->getFileName().contains(m_searchText, Qt::CaseInsensitive)) {
if (resPos >= m_filteredIndices.size() || m_filteredIndices[resPos] != actualIndex) {
break;
}
resPos++;
}
}
if (resPos == m_filteredIndices.size() && srcPos == sortedIndices.size()) {
return; // No change in filtered results
}
if (informView) {
emit layoutAboutToBeChanged();
}
m_filteredIndices.resize(resPos);
for (int i = srcPos; i < sortedIndices.size(); ++i) {
int actualIndex = m_currentSortReverse ? sortedIndices[sortedIndices.size() - 1 - i] : sortedIndices[i];
const auto& item = m_data[actualIndex]; const auto& item = m_data[actualIndex];
if (item->getFileName().contains(m_searchText, Qt::CaseInsensitive)) { if (item->getFileName().contains(m_searchText, Qt::CaseInsensitive)) {
m_filteredIndices.append(actualIndex); m_filteredIndices.append(actualIndex);
} }
} }
if (informView) {
emit layoutChanged(); emit layoutChanged();
}
} }
void WallReel::Core::Image::Model::_onProgressValueChanged(int value) { void WallReel::Core::Image::Model::_onProgressValueChanged(int value) {
@@ -311,7 +335,7 @@ void WallReel::Core::Image::Model::_onProcessingFinished() {
_updateSortIndices(static_cast<Config::SortType>(i)); _updateSortIndices(static_cast<Config::SortType>(i));
} }
_applySearchFilter(); _applySearchFilter(false);
endResetModel(); endResetModel();
@@ -320,7 +344,6 @@ void WallReel::Core::Image::Model::_onProcessingFinished() {
m_isLoading = false; m_isLoading = false;
m_progressUpdateTimer.stop(); m_progressUpdateTimer.stop();
emit progressChanged(); emit progressChanged();
// emit isLoadingChanged();
// QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() { // QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() {
// emit isLoadingChanged(); // emit isLoadingChanged();
// }); // });
@@ -329,8 +352,9 @@ void WallReel::Core::Image::Model::_onProcessingFinished() {
void WallReel::Core::Image::Model::_onSortMethodChanged() { void WallReel::Core::Image::Model::_onSortMethodChanged() {
_applySearchFilter(); _applySearchFilter();
emit focusedImageChanged();
} }
void WallReel::Core::Image::Model::_onSearchTextChanged() { void WallReel::Core::Image::Model::_onSearchTextChanged() {
_updateFocusedName(); emit focusedImageChanged();
} }
+10 -5
View File
@@ -78,23 +78,24 @@ class Model : public QAbstractListModel {
Q_INVOKABLE void setSearchText(const QString& text); Q_INVOKABLE void setSearchText(const QString& text);
const Data* getDataPtrAt(int index) const; Data* imageAt(int index);
Data* focusedImage();
Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const; Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const;
void loadAndProcess(const QStringList& paths); void loadAndProcess(const QStringList& paths);
int fromProxyIndex(int proxyIndex) const;
Q_INVOKABLE void focusOnIndex(int index); Q_INVOKABLE void focusOnIndex(int index);
Q_INVOKABLE void stop(); Q_INVOKABLE void stop();
private: private:
int _convertProxyIndex(int proxyIndex) const;
void _clearData(); void _clearData();
void _updateSortIndices(Config::SortType type); void _updateSortIndices(Config::SortType type);
void _updateFocusedName(); void _updateFocusedProperties();
void _applySearchFilter(); void _applySearchFilter(bool informView = true);
signals: signals:
void isLoadingChanged(); void isLoadingChanged();
@@ -104,6 +105,7 @@ class Model : public QAbstractListModel {
void currentSortReverseChanged(); void currentSortReverseChanged();
void focusedNameChanged(); void focusedNameChanged();
void searchTextChanged(); void searchTextChanged();
void focusedImageChanged();
private slots: private slots:
void _onProgressValueChanged(int value); void _onProgressValueChanged(int value);
@@ -129,6 +131,9 @@ class Model : public QAbstractListModel {
// QTimer m_searchDebounceTimer; // QTimer m_searchDebounceTimer;
// static constexpr int s_SearchDebounceIntervalMs = 300; // static constexpr int s_SearchDebounceIntervalMs = 300;
QColor m_focusedColor{};
QString m_focusedColorName{};
QFutureWatcher<Data*> m_watcher; QFutureWatcher<Data*> m_watcher;
bool m_isLoading = false; bool m_isLoading = false;
+6
View File
@@ -1,6 +1,8 @@
#ifndef WALLREEL_PALETTE_DATA_HPP #ifndef WALLREEL_PALETTE_DATA_HPP
#define WALLREEL_PALETTE_DATA_HPP #define WALLREEL_PALETTE_DATA_HPP
#include <qcolor.h>
#include <QColor> #include <QColor>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
@@ -37,6 +39,10 @@ struct PaletteItem {
} }
return QColor(); return QColor();
} }
bool operator==(const PaletteItem& other) const {
return name == other.name;
}
}; };
} // namespace WallReel::Core::Palette } // namespace WallReel::Core::Palette
+63 -1
View File
@@ -1,10 +1,16 @@
#include "manager.hpp" #include "manager.hpp"
#include "Palette/matchcolor.hpp"
#include "Utils/misc.hpp"
#include "logger.hpp"
#include "predefined.hpp" #include "predefined.hpp"
WallReel::Core::Palette::Manager::Manager( WallReel::Core::Palette::Manager::Manager(
const Config::PaletteConfigItems& config, const Config::PaletteConfigItems& config,
QObject* parent) : QObject(parent) { Image::Model& imageModel,
QObject* parent) : QObject(parent), m_imageModel(imageModel) {
connect(&m_imageModel, &Image::Model::focusedImageChanged, this, &Manager::updateColor);
// The new ones overrides the old ones, use a hashtable to track // The new ones overrides the old ones, use a hashtable to track
// the latest index of each palette name, then only insert the // the latest index of each palette name, then only insert the
// ones whose index matches the latest index in the hashtable // ones whose index matches the latest index in the hashtable
@@ -40,3 +46,59 @@ WallReel::Core::Palette::Manager::Manager(
} }
} }
} }
void WallReel::Core::Palette::Manager::updateColor() {
bool hasResult = false;
Utils::Defer defer([&]() {
if (!hasResult) {
m_displayColor = QColor();
m_displayColorName = "";
}
emit colorChanged();
emit colorNameChanged();
});
auto imageData = m_imageModel.focusedImage();
if (!imageData || !imageData->isValid()) {
return;
}
// No palette selected, use dominant color
if (!m_selectedPalette.has_value()) {
m_displayColor = imageData->getDominantColor();
m_displayColorName = "";
hasResult = true;
return;
}
// Only palette selected, use the colosest color in the palette
if (!m_selectedColor.has_value()) {
auto cached = imageData->getCachedColor(m_selectedPalette->name);
if (cached.has_value()) {
auto it = std::find_if(m_selectedPalette->colors.begin(),
m_selectedPalette->colors.end(),
[&](const ColorItem& item) {
return item.name == cached.value();
});
if (it != m_selectedPalette->colors.end()) {
Logger::debug("Using cached color match for image " + imageData->getFileName() +
": " + it->name);
m_displayColor = it->color;
m_displayColorName = it->name;
hasResult = true;
return;
}
}
auto matched = bestMatch(
imageData->getDominantColor(),
m_selectedPalette.value().colors);
Logger::debug("Computed color match for image " + imageData->getFileName() + ": " +
matched.name);
imageData->cacheColor(m_selectedPalette->name, matched.name);
m_displayColor = matched.color;
m_displayColorName = matched.name;
hasResult = true;
return;
}
// Both are set, use them
m_displayColor = m_selectedColor.value().color;
m_displayColorName = m_selectedColor.value().name;
hasResult = true;
}
+58 -5
View File
@@ -1,7 +1,10 @@
#ifndef WALLREEL_PALETTE_MANAGER_HPP #ifndef WALLREEL_PALETTE_MANAGER_HPP
#define WALLREEL_PALETTE_MANAGER_HPP #define WALLREEL_PALETTE_MANAGER_HPP
#include <qcolor.h>
#include "Config/data.hpp" #include "Config/data.hpp"
#include "Image/model.hpp"
#include "data.hpp" #include "data.hpp"
namespace WallReel::Core::Palette { namespace WallReel::Core::Palette {
@@ -9,24 +12,74 @@ namespace WallReel::Core::Palette {
class Manager : public QObject { class Manager : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QList<PaletteItem> availablePalettes READ availablePalettes CONSTANT) Q_PROPERTY(QList<PaletteItem> availablePalettes READ availablePalettes CONSTANT)
Q_PROPERTY(QColor color READ color NOTIFY colorChanged)
Q_PROPERTY(QString colorName READ colorName NOTIFY colorNameChanged)
public: public:
Manager(const Config::PaletteConfigItems& config, Manager(const Config::PaletteConfigItems& config,
Image::Model& imageModel,
QObject* parent = nullptr); QObject* parent = nullptr);
const QList<PaletteItem>& availablePalettes() const { const QList<PaletteItem>& availablePalettes() const {
return m_palettes; return m_palettes;
} }
Q_INVOKABLE PaletteItem getPalette(const QString& name) const { const QColor& color() const {
for (const auto& p : m_palettes) { return m_displayColor;
if (p.name == name) return p;
}
return PaletteItem();
} }
const QString& colorName() const {
return m_displayColorName;
}
Q_INVOKABLE void setSelectedPalette(const QVariant& paletteVar) {
if (paletteVar.isNull() || !paletteVar.isValid()) {
m_selectedPalette = std::nullopt;
} else {
m_selectedPalette = paletteVar.value<PaletteItem>();
}
updateColor();
}
Q_INVOKABLE void setSelectedColor(const QVariant& colorVar) {
if (colorVar.isNull() || !colorVar.isValid()) {
m_selectedColor = std::nullopt;
} else {
m_selectedColor = colorVar.value<ColorItem>();
}
updateColor();
}
QString getSelectedPaletteName() const {
return m_selectedPalette ? m_selectedPalette->name : QString();
}
QString getCurrentColorName() const {
return m_displayColorName;
}
QString getCurrentColorHex() const {
return m_displayColor.isValid()
? m_displayColor.name()
: QString();
}
public slots:
void updateColor();
signals:
void colorChanged();
void colorNameChanged();
private: private:
Image::Model& m_imageModel;
QList<PaletteItem> m_palettes; QList<PaletteItem> m_palettes;
std::optional<PaletteItem> m_selectedPalette = std::nullopt;
std::optional<ColorItem> m_selectedColor = std::nullopt;
QColor m_displayColor;
QString m_displayColorName;
}; };
} // namespace WallReel::Core::Palette } // namespace WallReel::Core::Palette
+70
View File
@@ -0,0 +1,70 @@
#include "matchcolor.hpp"
#include <QDebug>
#include <cmath>
#include <limits>
namespace WallReel::Core::Palette {
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates) {
if (candidates.isEmpty() || !target.isValid()) {
static ColorItem emptyItem;
return emptyItem;
}
int target_r = target.red();
int target_g = target.green();
int target_b = target.blue();
int target_h = target.hsvHue();
double target_s = target.hsvSaturationF();
const ColorItem* closest_flavor = nullptr;
double min_distance = std::numeric_limits<double>::max();
for (const auto& candidate : candidates) {
QColor p_color = candidate.color;
int p_r = p_color.red();
int p_g = p_color.green();
int p_b = p_color.blue();
int p_h = p_color.hsvHue();
// RGB distance with weighting
double rmean = (target_r + p_r) / 2.0;
double dr = target_r - p_r;
double dg = target_g - p_g;
double db = target_b - p_b;
double rgb_distance = std::sqrt((2.0 + rmean / 256.0) * dr * dr + 4.0 * dg * dg +
(2.0 + (255.0 - rmean) / 256.0) * db * db);
// Hue difference (with wrapping)
double hue_diff = 0.0;
if (target_h != -1 && p_h != -1) {
hue_diff = std::abs(target_h - p_h);
if (hue_diff > 180.0) {
hue_diff = 360.0 - hue_diff;
}
}
// Increase hue weight when saturation is high
double hue_weight = (target_s > 0.20) ? 2.0 : 0.5;
double total_distance = rgb_distance + (hue_diff * hue_weight * 3.0);
if (total_distance < min_distance) {
min_distance = total_distance;
closest_flavor = &candidate;
}
}
if (closest_flavor) {
return *closest_flavor;
}
static ColorItem emptyItem;
return emptyItem;
}
} // namespace WallReel::Core::Palette
+12
View File
@@ -0,0 +1,12 @@
#ifndef WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP
#define WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP
#include "data.hpp"
namespace WallReel::Core::Palette {
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates);
} // namespace WallReel::Core::Palette
#endif // WALLREEL_CORE_PALETTE_MATCHCOLOR_HPP
+38 -14
View File
@@ -6,6 +6,7 @@
#include "Config/data.hpp" #include "Config/data.hpp"
#include "Image/model.hpp" #include "Image/model.hpp"
#include "Palette/manager.hpp"
#include "Service/wallpaper.hpp" #include "Service/wallpaper.hpp"
#include "logger.hpp" #include "logger.hpp"
@@ -19,10 +20,16 @@ class Manager : public QObject {
public: public:
Manager( Manager(
const Config::ActionConfigItems& actionConfig, const Config::ActionConfigItems& actionConfig,
const Image::Model& imageModel, Image::Model& imageModel,
QObject* parent = nullptr) : m_imageModel(imageModel) { Palette::Manager& paletteManager,
m_wallpaperService = new WallpaperService(actionConfig, this); QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageModel(imageModel), m_paletteManager(paletteManager) {
m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, this);
// Listen on image change
connect(&m_imageModel, &Image::Model::focusedImageChanged, this, &Manager::previewWallpaper);
// Listen on palette change
connect(&m_paletteManager, &Palette::Manager::colorChanged, this, &Manager::previewWallpaper);
connect(&m_paletteManager, &Palette::Manager::colorNameChanged, this, &Manager::previewWallpaper);
// Forward signals // Forward signals
// Direct signal 2 signal connection // Direct signal 2 signal connection
connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted); connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted);
@@ -38,7 +45,7 @@ class Manager : public QObject {
} }
m_isProcessing = true; m_isProcessing = true;
emit isProcessingChanged(); emit isProcessingChanged();
const auto* data = m_imageModel.getDataPtrAt(index); const auto* data = m_imageModel.imageAt(index);
if (data) { if (data) {
m_wallpaperService->select(*data); m_wallpaperService->select(*data);
} else { } else {
@@ -48,15 +55,6 @@ class Manager : public QObject {
} }
} }
Q_INVOKABLE void previewWallpaper(int index) {
const auto* data = m_imageModel.getDataPtrAt(index);
if (data) {
m_wallpaperService->preview(*data);
} else {
emit previewCompleted();
}
}
Q_INVOKABLE void restore() { Q_INVOKABLE void restore() {
if (m_isProcessing) { if (m_isProcessing) {
Logger::debug("Already processing an action, ignoring restore request"); Logger::debug("Already processing an action, ignoring restore request");
@@ -67,8 +65,31 @@ class Manager : public QObject {
m_wallpaperService->restore(); m_wallpaperService->restore();
} }
Q_INVOKABLE void cancel() {
m_wallpaperService->stopAll();
if (m_actionConfig.restoreOnCancel) {
connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, [this]() {
emit cancelCompleted();
});
restore();
} else {
emit cancelCompleted();
}
}
bool isProcessing() const { return m_isProcessing; } bool isProcessing() const { return m_isProcessing; }
public slots:
void previewWallpaper() {
const auto* data = m_imageModel.focusedImage();
if (data) {
m_wallpaperService->preview(*data);
} else {
emit previewCompleted();
}
}
private slots: private slots:
void _onSelectCompleted() { void _onSelectCompleted() {
@@ -91,10 +112,13 @@ class Manager : public QObject {
void selectCompleted(); void selectCompleted();
void previewCompleted(); void previewCompleted();
void restoreCompleted(); void restoreCompleted();
void cancelCompleted();
private: private:
WallpaperService* m_wallpaperService; WallpaperService* m_wallpaperService;
const Image::Model& m_imageModel; const Config::ActionConfigItems& m_actionConfig;
Image::Model& m_imageModel;
Palette::Manager& m_paletteManager;
bool m_isProcessing = false; bool m_isProcessing = false;
}; };
+49 -9
View File
@@ -8,8 +8,9 @@
WallReel::Core::Service::WallpaperService::WallpaperService( WallReel::Core::Service::WallpaperService::WallpaperService(
const Config::ActionConfigItems& actionConfig, const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager,
QObject* parent) QObject* parent)
: QObject(parent), m_actionConfig(actionConfig) { : 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(m_actionConfig.previewDebounceTime);
@@ -48,6 +49,22 @@ WallReel::Core::Service::WallpaperService::WallpaperService(
}); });
} }
void WallReel::Core::Service::WallpaperService::stopAll() {
if (m_previewProcess->state() != QProcess::NotRunning) {
m_previewProcess->kill();
m_previewProcess->waitForFinished();
}
if (m_selectProcess->state() != QProcess::NotRunning) {
m_selectProcess->kill();
m_selectProcess->waitForFinished();
}
if (m_restoreProcess->state() != QProcess::NotRunning) {
m_restoreProcess->kill();
m_restoreProcess->waitForFinished();
}
m_previewDebounceTimer->stop();
}
void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) { void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) {
m_pendingImageData = &imageData; m_pendingImageData = &imageData;
m_previewDebounceTimer->start(); m_previewDebounceTimer->start();
@@ -69,6 +86,32 @@ void WallReel::Core::Service::WallpaperService::restore() {
_doRestore(); _doRestore();
} }
QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) {
auto palette = m_paletteManager.getSelectedPaletteName();
if (palette.isEmpty()) {
palette = "null";
}
auto color = m_paletteManager.getCurrentColorName();
if (color.isEmpty()) {
color = "null";
}
auto hex = m_paletteManager.getCurrentColorHex();
if (hex.isEmpty()) {
hex = "null";
}
return {
{"path", imageData.getFullPath()},
{"name", imageData.getFileName()},
{"size", QString::number(imageData.getSize())},
{"width", QString::number(imageData.getImage().width())},
{"height", QString::number(imageData.getImage().height())},
{"palette", palette},
{"color", color},
{"colorHex", hex},
{"domColor", m_paletteManager.color().name()},
};
}
void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& imageData) { void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& imageData) {
QString path = imageData.getFullPath(); QString path = imageData.getFullPath();
@@ -81,15 +124,13 @@ void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& im
std::cout << path.toStdString() << std::endl; std::cout << path.toStdString() << std::endl;
} }
const QHash<QString, QString> variables{ const auto variables = _generateVariables(imageData);
{"path", path},
{"name", imageData.getFileName()},
};
auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables); auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables);
if (command.isEmpty()) { if (command.isEmpty()) {
emit previewCompleted(); emit previewCompleted();
return; return;
} }
Logger::debug(QString("Executing preview command: %1").arg(command));
if (m_previewProcess->state() != QProcess::NotRunning) { if (m_previewProcess->state() != QProcess::NotRunning) {
m_previewProcess->kill(); m_previewProcess->kill();
@@ -110,15 +151,13 @@ void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& ima
std::cout << path.toStdString() << std::endl; std::cout << path.toStdString() << std::endl;
} }
const QHash<QString, QString> variables{ const auto variables = _generateVariables(imageData);
{"path", path},
{"name", imageData.getFileName()},
};
auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables); auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables);
if (command.isEmpty()) { if (command.isEmpty()) {
emit selectCompleted(); emit selectCompleted();
return; return;
} }
Logger::debug(QString("Executing select command: %1").arg(command));
m_selectProcess->start("sh", QStringList() << "-c" << command); m_selectProcess->start("sh", QStringList() << "-c" << command);
} }
@@ -133,5 +172,6 @@ void WallReel::Core::Service::WallpaperService::_doRestore() {
emit restoreCompleted(); emit restoreCompleted();
return; return;
} }
Logger::debug(QString("Executing restore command: %1").arg(command));
m_restoreProcess->start("sh", QStringList() << "-c" << command); m_restoreProcess->start("sh", QStringList() << "-c" << command);
} }
+6
View File
@@ -6,6 +6,7 @@
#include "Config/data.hpp" #include "Config/data.hpp"
#include "Image/data.hpp" #include "Image/data.hpp"
#include "Palette/manager.hpp"
namespace WallReel::Core::Service { namespace WallReel::Core::Service {
@@ -15,8 +16,11 @@ class WallpaperService : public QObject {
public: public:
WallpaperService( WallpaperService(
const Config::ActionConfigItems& actionConfig, const Config::ActionConfigItems& actionConfig,
const Palette::Manager& paletteManager,
QObject* parent = nullptr); QObject* parent = nullptr);
void stopAll();
public slots: public slots:
void preview(const Image::Data& imageData); // execute after 500ms of inactivity void preview(const Image::Data& imageData); // execute after 500ms of inactivity
void select(const Image::Data& imageData); // execute immediately, ignore if already running void select(const Image::Data& imageData); // execute immediately, ignore if already running
@@ -31,8 +35,10 @@ class WallpaperService : public QObject {
void _doPreview(const Image::Data& imageData); void _doPreview(const Image::Data& imageData);
void _doSelect(const Image::Data& imageData); void _doSelect(const Image::Data& imageData);
void _doRestore(); void _doRestore();
QHash<QString, QString> _generateVariables(const Image::Data& imageData);
const Config::ActionConfigItems& m_actionConfig; const Config::ActionConfigItems& m_actionConfig;
const Palette::Manager& m_paletteManager;
QTimer* m_previewDebounceTimer; QTimer* m_previewDebounceTimer;
const Image::Data* m_pendingImageData; const Image::Data* m_pendingImageData;
QProcess* m_previewProcess; QProcess* m_previewProcess;
+1 -1
View File
@@ -8,10 +8,10 @@ qt_add_qml_module(${UILIB_NAME}
Providers/CarouselProvider.qml Providers/CarouselProvider.qml
Modules/Carousel.qml Modules/Carousel.qml
Modules/TitleBar.qml Modules/TitleBar.qml
Modules/SearchBar.qml
Modules/SortControl.qml Modules/SortControl.qml
Modules/ColorControl.qml Modules/ColorControl.qml
Modules/TopBar.qml Modules/TopBar.qml
Modules/BottomBar.qml Modules/BottomBar.qml
Components/WRSearchBar.qml
Components/WRTextButton.qml Components/WRTextButton.qml
) )
+2 -2
View File
@@ -28,7 +28,7 @@ Item {
ComboBox { ComboBox {
id: paletteCombo id: paletteCombo
implicitWidth: 110 implicitWidth: 200
// -1 means nothing selected // -1 means nothing selected
currentIndex: -1 currentIndex: -1
displayText: currentIndex < 0 ? "— palette —" : currentText displayText: currentIndex < 0 ? "— palette —" : currentText
@@ -77,7 +77,7 @@ Item {
if (root.colorHex.length > 0) if (root.colorHex.length > 0)
return root.colorName.length > 0 ? root.colorName + " " + root.colorHex : root.colorHex; return root.colorName.length > 0 ? root.colorName + " " + root.colorHex : root.colorHex;
return root.colorName == "Auto" ? "" : root.colorName; return root.colorName;
} }
visible: root.colorName.length > 0 || root.colorHex.length > 0 visible: root.colorName.length > 0 || root.colorHex.length > 0
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import WallReel.UI.Components
Item { Item {
id: root id: root
@@ -43,7 +44,7 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
SearchBar { WRSearchBar {
id: searchBar id: searchBar
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
+17 -11
View File
@@ -22,13 +22,13 @@ QtObject {
property string selectedSortType: ImageModel.currentSortType property string selectedSortType: ImageModel.currentSortType
property bool isSortReverse: ImageModel.currentSortReverse property bool isSortReverse: ImageModel.currentSortReverse
//// Palette / Color //// Palette / Color
readonly property var availablePalettes: [] readonly property var availablePalettes: PaletteManager.availablePalettes
property var selectedPalette: null // PaletteItem | null property var selectedPalette: null // PaletteItem | null
readonly property var availableColors: selectedPalette ? selectedPalette.colors : [] readonly property var availableColors: selectedPalette ? selectedPalette.colors : []
property var selectedColor: null // ColorItem | null (null means "auto") property var selectedColor: null // ColorItem | null (null means "auto")
readonly property string colorName: selectedColor ? selectedColor.name : "Auto" readonly property string colorName: PaletteManager.colorName
readonly property string colorHex: selectedColor ? selectedColor.color.toString().toUpperCase() : "" readonly property string colorHex: PaletteManager.color
readonly property color colorValue: selectedColor ? selectedColor.color : "transparent" readonly property color colorValue: PaletteManager.color
//// Actions state //// Actions state
readonly property bool isProcessing: ServiceManager.isProcessing readonly property bool isProcessing: ServiceManager.isProcessing
@@ -42,7 +42,7 @@ QtObject {
} }
function cancel() { function cancel() {
Qt.quit(); ServiceManager.cancel();
} }
function focusSearch() { function focusSearch() {
@@ -68,19 +68,25 @@ QtObject {
function setSearchText(text) { function setSearchText(text) {
ImageModel.setSearchText(text); ImageModel.setSearchText(text);
currentIndex = 0; // reset index when search text changes if (currentIndex != 0)
currentIndex = 0;
} }
onCurrentIndexChanged: () => { onCurrentIndexChanged: () => {
if (!isLoading) { if (!isLoading)
ServiceManager.previewWallpaper(currentIndex);
ImageModel.focusOnIndex(currentIndex); ImageModel.focusOnIndex(currentIndex);
}
} }
Component.onCompleted: () => { Component.onCompleted: () => {
if (!isLoading) { if (!isLoading)
ServiceManager.previewWallpaper(currentIndex);
ImageModel.focusOnIndex(currentIndex); ImageModel.focusOnIndex(currentIndex);
} }
onSelectedPaletteChanged: () => {
PaletteManager.setSelectedPalette(selectedPalette);
}
onSelectedColorChanged: () => {
PaletteManager.setSelectedColor(selectedColor);
} }
} }
+18 -8
View File
@@ -1,3 +1,5 @@
#include <qobject.h>
#include <QApplication> #include <QApplication>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
@@ -46,13 +48,6 @@ int main(int argc, char* argv[]) {
"Config", "Config",
config); config);
auto paletteMgr = new Palette::Manager(
config->getPaletteConfig(),
&a);
engine.rootContext()->setContextProperty("PaletteManager", paletteMgr);
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
qRegisterMetaType<Palette::ColorItem>("ColorItem");
auto imageModel = new Image::Model( auto imageModel = new Image::Model(
*imageProvider, *imageProvider,
config->getSortConfig(), config->getSortConfig(),
@@ -65,10 +60,19 @@ int main(int argc, char* argv[]) {
"ImageModel", "ImageModel",
imageModel); imageModel);
auto paletteMgr = new Palette::Manager(
config->getPaletteConfig(),
*imageModel,
imageModel);
engine.rootContext()->setContextProperty("PaletteManager", paletteMgr);
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
qRegisterMetaType<Palette::ColorItem>("ColorItem");
auto Service = new Service::Manager( auto Service = new Service::Manager(
config->getActionConfig(), config->getActionConfig(),
*imageModel, *imageModel,
imageModel); *paletteMgr,
paletteMgr);
qmlRegisterSingletonInstance( qmlRegisterSingletonInstance(
COREMODULE_URI, COREMODULE_URI,
MODULE_VERSION_MAJOR, MODULE_VERSION_MAJOR,
@@ -82,6 +86,11 @@ int main(int argc, char* argv[]) {
&a, &a,
[]() { QCoreApplication::quit(); }); []() { QCoreApplication::quit(); });
} }
QObject::connect(
Service,
&Service::Manager::cancelCompleted,
&a,
[]() { QCoreApplication::quit(); });
QObject::connect( QObject::connect(
&engine, &engine,
@@ -91,6 +100,7 @@ int main(int argc, char* argv[]) {
Qt::QueuedConnection); Qt::QueuedConnection);
engine.loadFromModule(UIMODULE_URI, "Main"); engine.loadFromModule(UIMODULE_URI, "Main");
config->captureState();
imageModel->loadAndProcess(config->getWallpapers()); imageModel->loadAndProcess(config->getWallpapers());
return a.exec(); return a.exec();