diff --git a/CMakeLists.txt b/CMakeLists.txt index 8303361..fe906a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/Tests/tst_configmgr.cpp b/Tests/tst_configmgr.cpp index 267bba1..0f7cc5c 100644 --- a/Tests/tst_configmgr.cpp +++ b/Tests/tst_configmgr.cpp @@ -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() { diff --git a/WallReel/Core/CMakeLists.txt b/WallReel/Core/CMakeLists.txt index 444bdbb..c91d5d1 100644 --- a/WallReel/Core/CMakeLists.txt +++ b/WallReel/Core/CMakeLists.txt @@ -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 diff --git a/WallReel/Core/Config/data.hpp b/WallReel/Core/Config/data.hpp new file mode 100644 index 0000000..fe8f209 --- /dev/null +++ b/WallReel/Core/Config/data.hpp @@ -0,0 +1,119 @@ +#ifndef WALLREEL_CONFIG_DATA_HPP +#define WALLREEL_CONFIG_DATA_HPP + +#include +#include +#include +#include +#include +#include +#include + +// 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 dirs; + QList excludes; +}; + +struct PaletteConfigItems { + struct PaletteColorConfigItem { + QString name; + QColor value; + }; + + struct PaletteConfigItem { + QString name; + QList colors; + }; + + QList palettes; +}; + +struct ActionConfigItems { + struct SaveStateItem { + QString key; + QString defaultVal; + QString cmd; + int timeout = 3000; + }; + + QList saveStateConfig; + QHash 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 diff --git a/WallReel/Core/Config/manager.cpp b/WallReel/Core/Config/manager.cpp index a4db166..7418724 100644 --- a/WallReel/Core/Config/manager.cpp +++ b/WallReel/Core/Config/manager.cpp @@ -5,8 +5,10 @@ #include #include #include +#include #include #include +#include #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::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(); + } +} diff --git a/WallReel/Core/Config/manager.hpp b/WallReel/Core/Config/manager.hpp index 32795ef..bae8f1b 100644 --- a/WallReel/Core/Config/manager.hpp +++ b/WallReel/Core/Config/manager.hpp @@ -1,107 +1,10 @@ #ifndef WALLREEL_CONFIGMGR_HPP #define WALLREEL_CONFIGMGR_HPP -#include -#include -#include -#include -#include -#include - -// 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 dirs; - QList excludes; -}; - -struct PaletteConfigItems { - struct PaletteColorConfigItem { - QString name; - QColor value; - }; - - struct PaletteConfigItem { - QString name; - QList colors; - }; - - QList palettes; -}; - -struct ActionConfigItems { - - QHash 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 diff --git a/WallReel/Core/Image/model.hpp b/WallReel/Core/Image/model.hpp index 4d39ccf..1efe318 100644 --- a/WallReel/Core/Image/model.hpp +++ b/WallReel/Core/Image/model.hpp @@ -6,7 +6,7 @@ #include #include -#include "Config/manager.hpp" +#include "Config/data.hpp" #include "provider.hpp" namespace WallReel::Core::Image { diff --git a/WallReel/Core/Palette/domcolor.cpp b/WallReel/Core/Palette/domcolor.cpp new file mode 100644 index 0000000..444ca60 --- /dev/null +++ b/WallReel/Core/Palette/domcolor.cpp @@ -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 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); +} diff --git a/WallReel/Core/Palette/domcolor.hpp b/WallReel/Core/Palette/domcolor.hpp new file mode 100644 index 0000000..fc27f76 --- /dev/null +++ b/WallReel/Core/Palette/domcolor.hpp @@ -0,0 +1,12 @@ +#ifndef WALLREEL_CORE_PALETTE_DOMCOLOR_HPP +#define WALLREEL_CORE_PALETTE_DOMCOLOR_HPP + +#include + +namespace WallReel::Core::Palette { + +QColor getDominantColor(const QImage& image); + +} // namespace WallReel::Core::Palette + +#endif // WALLREEL_CORE_PALETTE_DOMCOLOR_HPP diff --git a/WallReel/Core/Palette/manager.hpp b/WallReel/Core/Palette/manager.hpp index fca4cf1..c2775bb 100644 --- a/WallReel/Core/Palette/manager.hpp +++ b/WallReel/Core/Palette/manager.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 { diff --git a/WallReel/Core/Utils/colorextractor.hpp b/WallReel/Core/Utils/colorextractor.hpp deleted file mode 100644 index 8b13789..0000000 --- a/WallReel/Core/Utils/colorextractor.hpp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/WallReel/Core/wallpaperservice.hpp b/WallReel/Core/wallpaperservice.hpp index aac6c0f..a323292 100644 --- a/WallReel/Core/wallpaperservice.hpp +++ b/WallReel/Core/wallpaperservice.hpp @@ -4,7 +4,7 @@ #include #include -#include "Config/manager.hpp" +#include "Config/data.hpp" #include "Image/data.hpp" namespace WallReel::Core { diff --git a/WallReel/UI/CMakeLists.txt b/WallReel/UI/CMakeLists.txt index 20b2745..ff8924a 100644 --- a/WallReel/UI/CMakeLists.txt +++ b/WallReel/UI/CMakeLists.txt @@ -6,4 +6,5 @@ qt_add_qml_module(${UILIB_NAME} Pages/LoadingScreen.qml Pages/CarouselScreen.qml Modules/Carousel.qml + Modules/TitleBar.qml ) diff --git a/WallReel/UI/Main.qml b/WallReel/UI/Main.qml index feea694..052fe2c 100644 --- a/WallReel/UI/Main.qml +++ b/WallReel/UI/Main.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") diff --git a/WallReel/UI/Modules/TitleBar.qml b/WallReel/UI/Modules/TitleBar.qml new file mode 100644 index 0000000..cc4a8e6 --- /dev/null +++ b/WallReel/UI/Modules/TitleBar.qml @@ -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 + } + +} diff --git a/WallReel/UI/Pages/CarouselScreen.qml b/WallReel/UI/Pages/CarouselScreen.qml index 85bb6ac..b71138e 100644 --- a/WallReel/UI/Pages/CarouselScreen.qml +++ b/WallReel/UI/Pages/CarouselScreen.qml @@ -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 { diff --git a/misc/ColorArgTest/index.html b/misc/ColorArgTest/index.html new file mode 100644 index 0000000..e1bc9d4 --- /dev/null +++ b/misc/ColorArgTest/index.html @@ -0,0 +1,216 @@ + + + + + + Color Arg Test + + + +
+ +
+ Image Preview +
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + + + + +