🚧 wip: che kku po in to
This commit is contained in:
+16
-1
@@ -13,6 +13,8 @@ set(MODULE_VERSION_MINOR 0)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE)
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
set(CMAKE_BUILD_TYPE Release)
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
endif()
|
endif()
|
||||||
@@ -23,15 +25,28 @@ endif()
|
|||||||
|
|
||||||
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent)
|
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent Test)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.5)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
|
|
||||||
qt_policy(SET QTP0004 NEW)
|
qt_policy(SET QTP0004 NEW)
|
||||||
|
|
||||||
|
option(BUILD_TESTING "Build the testing tree." ON)
|
||||||
|
option(ADDRESS_SANITIZER "Enable Address Sanitizer for debugging." OFF)
|
||||||
|
|
||||||
add_subdirectory(WallReel/Core)
|
add_subdirectory(WallReel/Core)
|
||||||
add_subdirectory(WallReel/UI)
|
add_subdirectory(WallReel/UI)
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(Tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ADDRESS_SANITIZER)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(${EXECUTABLE_NAME}
|
add_executable(${EXECUTABLE_NAME}
|
||||||
WallReel/main.cpp
|
WallReel/main.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
project(Tests LANGUAGES CXX)
|
||||||
|
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Core Test)
|
||||||
|
|
||||||
|
add_executable(tst_configmgr
|
||||||
|
tst_configmgr.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(tst_imagemodel
|
||||||
|
tst_imagemodel.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME tst_configmgr COMMAND tst_configmgr)
|
||||||
|
add_test(NAME tst_imagemodel COMMAND tst_imagemodel)
|
||||||
|
|
||||||
|
target_link_libraries(tst_configmgr PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Test
|
||||||
|
Qt6::Gui
|
||||||
|
wallreel-core
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(tst_imagemodel PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Test
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::Quick
|
||||||
|
wallreel-core
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(tst_configmgr PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/WallReel/Core
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(tst_imagemodel PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/WallReel/Core
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
)
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
#include <QtGui/qcolor.h>
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "configmgr.hpp"
|
||||||
|
|
||||||
|
class TestConfigMgr : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
void cleanupTestCase();
|
||||||
|
|
||||||
|
void testDefaults();
|
||||||
|
void testFullConfigParsing();
|
||||||
|
void testInvalidConfigValues();
|
||||||
|
void testWallpaperScanRecursive();
|
||||||
|
void testWallpaperScanNonRecursive();
|
||||||
|
void testWallpaperExcludes();
|
||||||
|
void testExplicitPaths();
|
||||||
|
void testImageExtensions();
|
||||||
|
void testSortTypes();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTemporaryDir m_tempDir;
|
||||||
|
QString m_configPath;
|
||||||
|
QString m_wallpaperRoot;
|
||||||
|
|
||||||
|
void writeConfig(const QJsonObject& json);
|
||||||
|
void createDummyFile(const QString& relPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestConfigMgr::initTestCase() {
|
||||||
|
QVERIFY(m_tempDir.isValid());
|
||||||
|
m_configPath = m_tempDir.path() + "/config.json";
|
||||||
|
m_wallpaperRoot = m_tempDir.path() + "/wallpapers";
|
||||||
|
QDir().mkpath(m_wallpaperRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::cleanupTestCase() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::writeConfig(const QJsonObject& json) {
|
||||||
|
QFile configFile(m_configPath);
|
||||||
|
QVERIFY(configFile.open(QIODevice::WriteOnly));
|
||||||
|
configFile.write(QJsonDocument(json).toJson());
|
||||||
|
configFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::createDummyFile(const QString& relPath) {
|
||||||
|
QString absPath = m_wallpaperRoot + "/" + relPath;
|
||||||
|
QFileInfo fi(absPath);
|
||||||
|
QDir().mkpath(fi.absolutePath());
|
||||||
|
QFile file(absPath);
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
file.write("foobar");
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testDefaults() {
|
||||||
|
// Empty config file
|
||||||
|
writeConfig(QJsonObject());
|
||||||
|
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
// Check Style Defaults
|
||||||
|
QCOMPARE(config.getImageWidth(), 320);
|
||||||
|
QCOMPARE(config.getImageHeight(), 200);
|
||||||
|
QCOMPARE(config.getImageFocusScale(), 1.5);
|
||||||
|
QCOMPARE(config.getWindowWidth(), 750);
|
||||||
|
QCOMPARE(config.getWindowHeight(), 500);
|
||||||
|
|
||||||
|
// Check Sort Defaults
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
|
||||||
|
QCOMPARE(config.getSortConfig().reverse, false);
|
||||||
|
|
||||||
|
// Check Action Defaults
|
||||||
|
QCOMPARE(config.getActionConfig().previewDebounceTime, 300);
|
||||||
|
QCOMPARE(config.getActionConfig().printSelected, false);
|
||||||
|
QCOMPARE(config.getActionConfig().printPreview, false);
|
||||||
|
QVERIFY(config.getActionConfig().onSelected.isEmpty());
|
||||||
|
QVERIFY(config.getActionConfig().onPreview.isEmpty());
|
||||||
|
QVERIFY(config.getActionConfig().onRestore.isEmpty());
|
||||||
|
QVERIFY(config.getActionConfig().saveState.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testFullConfigParsing() {
|
||||||
|
QJsonObject root;
|
||||||
|
|
||||||
|
// Wallpaper settings
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray dirsArray;
|
||||||
|
QJsonObject dir1;
|
||||||
|
dir1["path"] = "/tmp/w1";
|
||||||
|
dir1["recursive"] = true;
|
||||||
|
dirsArray.append(dir1);
|
||||||
|
wallpaperObj["dirs"] = dirsArray;
|
||||||
|
|
||||||
|
QJsonArray pathsArray;
|
||||||
|
pathsArray.append("/tmp/p1.jpg");
|
||||||
|
wallpaperObj["paths"] = pathsArray;
|
||||||
|
|
||||||
|
QJsonArray excludesArray;
|
||||||
|
excludesArray.append(".*bad.*");
|
||||||
|
wallpaperObj["excludes"] = excludesArray;
|
||||||
|
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
// Palette
|
||||||
|
QJsonArray palettesArray;
|
||||||
|
QJsonObject palette1;
|
||||||
|
palette1["name"] = "Default";
|
||||||
|
QJsonArray colorsArray;
|
||||||
|
QJsonObject color1;
|
||||||
|
color1["name"] = "Red";
|
||||||
|
color1["value"] = "#FF0000";
|
||||||
|
colorsArray.append(color1);
|
||||||
|
palette1["colors"] = colorsArray;
|
||||||
|
palettesArray.append(palette1);
|
||||||
|
root["palettes"] = palettesArray;
|
||||||
|
|
||||||
|
// Action
|
||||||
|
QJsonObject actionObj;
|
||||||
|
actionObj["printSelected"] = true;
|
||||||
|
actionObj["onSelected"] = "echo {{ path }}";
|
||||||
|
root["action"] = actionObj;
|
||||||
|
|
||||||
|
// Style
|
||||||
|
QJsonObject styleObj;
|
||||||
|
styleObj["image_width"] = 100;
|
||||||
|
styleObj["image_height"] = 100;
|
||||||
|
root["style"] = styleObj;
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "date";
|
||||||
|
sortObj["reverse"] = true;
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
// Assertions
|
||||||
|
QCOMPARE(config.getWallpaperConfig().dirs.size(), 1);
|
||||||
|
QCOMPARE(config.getWallpaperConfig().dirs[0].path, "/tmp/w1");
|
||||||
|
QCOMPARE(config.getWallpaperConfig().dirs[0].recursive, true);
|
||||||
|
|
||||||
|
QCOMPARE(config.getWallpaperConfig().paths.size(), 1);
|
||||||
|
QCOMPARE(config.getWallpaperConfig().paths[0], "/tmp/p1.jpg");
|
||||||
|
|
||||||
|
QCOMPARE(config.getWallpaperConfig().excludes.size(), 1);
|
||||||
|
QCOMPARE(config.getWallpaperConfig().excludes[0].pattern(), ".*bad.*");
|
||||||
|
|
||||||
|
QCOMPARE(config.getPaletteConfig().palettes.size(), 1);
|
||||||
|
QCOMPARE(config.getPaletteConfig().palettes[0].name, "Default");
|
||||||
|
QCOMPARE(config.getPaletteConfig().palettes[0].colors.size(), 1);
|
||||||
|
QCOMPARE(config.getPaletteConfig().palettes[0].colors[0].name, "Red");
|
||||||
|
QCOMPARE(config.getPaletteConfig().palettes[0].colors[0].value.name().toLower(), "#ff0000");
|
||||||
|
|
||||||
|
QCOMPARE(config.getActionConfig().printSelected, true);
|
||||||
|
QCOMPARE(config.getActionConfig().onSelected, "echo {{ path }}");
|
||||||
|
|
||||||
|
QCOMPARE(config.getImageWidth(), 100);
|
||||||
|
QCOMPARE(config.getImageHeight(), 100);
|
||||||
|
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
|
||||||
|
QCOMPARE(config.getSortConfig().reverse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testInvalidConfigValues() {
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject styleObj;
|
||||||
|
styleObj["image_width"] = "not a number"; // Should be ignored
|
||||||
|
root["style"] = styleObj;
|
||||||
|
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "invalid_type";
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
// Should retain defaults
|
||||||
|
QCOMPARE(config.getImageWidth(), 320);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testWallpaperScanRecursive() {
|
||||||
|
// Setup files
|
||||||
|
createDummyFile("rec/root.jpg");
|
||||||
|
createDummyFile("rec/sub/deep.png"); // should be found
|
||||||
|
createDummyFile("rec/ignore.txt"); // should be ignored
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray dirsArray;
|
||||||
|
QJsonObject dirConfig;
|
||||||
|
dirConfig["path"] = m_wallpaperRoot + "/rec";
|
||||||
|
dirConfig["recursive"] = true;
|
||||||
|
dirsArray.append(dirConfig);
|
||||||
|
wallpaperObj["dirs"] = dirsArray;
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
QStringList wallpapers = config.getWallpapers();
|
||||||
|
QCOMPARE(wallpapers.size(), 2);
|
||||||
|
|
||||||
|
// Sort to verify presence
|
||||||
|
wallpapers.sort();
|
||||||
|
// Paths are absolute
|
||||||
|
QVERIFY(wallpapers[0].endsWith("root.jpg"));
|
||||||
|
QVERIFY(wallpapers[1].endsWith("deep.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testWallpaperScanNonRecursive() {
|
||||||
|
createDummyFile("nonrec/root.jpg");
|
||||||
|
createDummyFile("nonrec/sub/deep.png");
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray dirsArray;
|
||||||
|
QJsonObject dirConfig;
|
||||||
|
dirConfig["path"] = m_wallpaperRoot + "/nonrec";
|
||||||
|
dirConfig["recursive"] = false;
|
||||||
|
dirsArray.append(dirConfig);
|
||||||
|
wallpaperObj["dirs"] = dirsArray;
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
QStringList wallpapers = config.getWallpapers();
|
||||||
|
QCOMPARE(wallpapers.size(), 1);
|
||||||
|
QVERIFY(wallpapers[0].endsWith("root.jpg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testWallpaperExcludes() {
|
||||||
|
createDummyFile("excl/good.jpg");
|
||||||
|
createDummyFile("excl/bad.jpg");
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray dirsArray;
|
||||||
|
QJsonObject dirConfig;
|
||||||
|
dirConfig["path"] = m_wallpaperRoot + "/excl";
|
||||||
|
dirConfig["recursive"] = false;
|
||||||
|
dirsArray.append(dirConfig);
|
||||||
|
wallpaperObj["dirs"] = dirsArray;
|
||||||
|
|
||||||
|
QJsonArray excludes;
|
||||||
|
excludes.append(".*bad\\.jpg$");
|
||||||
|
wallpaperObj["excludes"] = excludes;
|
||||||
|
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
QStringList wallpapers = config.getWallpapers();
|
||||||
|
QCOMPARE(wallpapers.size(), 1);
|
||||||
|
QVERIFY(wallpapers[0].endsWith("good.jpg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testExplicitPaths() {
|
||||||
|
createDummyFile("explicit/a.jpg");
|
||||||
|
QString absPath = m_wallpaperRoot + "/explicit/a.jpg";
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray pathsArray;
|
||||||
|
pathsArray.append(absPath);
|
||||||
|
wallpaperObj["paths"] = pathsArray;
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
QStringList wallpapers = config.getWallpapers();
|
||||||
|
QCOMPARE(wallpapers.size(), 1);
|
||||||
|
QCOMPARE(wallpapers[0], absPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testImageExtensions() {
|
||||||
|
createDummyFile("ext/image1.jpg");
|
||||||
|
createDummyFile("ext/image2.jpeg");
|
||||||
|
createDummyFile("ext/image3.png");
|
||||||
|
createDummyFile("ext/image4.bmp");
|
||||||
|
createDummyFile("ext/text.txt");
|
||||||
|
createDummyFile("ext/script.sh");
|
||||||
|
createDummyFile("ext/noext");
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject wallpaperObj;
|
||||||
|
QJsonArray dirsArray;
|
||||||
|
QJsonObject dirConfig;
|
||||||
|
dirConfig["path"] = m_wallpaperRoot + "/ext";
|
||||||
|
dirConfig["recursive"] = false;
|
||||||
|
dirsArray.append(dirConfig);
|
||||||
|
wallpaperObj["dirs"] = dirsArray;
|
||||||
|
root["wallpaper"] = wallpaperObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
|
||||||
|
QStringList wallpapers = config.getWallpapers();
|
||||||
|
|
||||||
|
int imageCount = 0;
|
||||||
|
for (const auto& w : wallpapers) {
|
||||||
|
if (w.endsWith(".txt") || w.endsWith(".sh") || w.endsWith("noext")) {
|
||||||
|
QFAIL(qPrintable("Found non-image file: " + w));
|
||||||
|
}
|
||||||
|
imageCount++;
|
||||||
|
}
|
||||||
|
QVERIFY(imageCount >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestConfigMgr::testSortTypes() {
|
||||||
|
// 1. None sort
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "none";
|
||||||
|
sortObj["reverse"] = false;
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::None);
|
||||||
|
QCOMPARE(config.getSortConfig().reverse, false);
|
||||||
|
}
|
||||||
|
// 2. Name sort (default)
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "name";
|
||||||
|
sortObj["reverse"] = true;
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
|
||||||
|
QCOMPARE(config.getSortConfig().reverse, true);
|
||||||
|
}
|
||||||
|
// 3. Size sort
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "size";
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Size);
|
||||||
|
}
|
||||||
|
// 4. Date sort
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "date";
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
|
||||||
|
}
|
||||||
|
// 5. Invalid sort -> fallback to default (Name)
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "invalid_blah";
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
// Default initialized in Config constructor is Name
|
||||||
|
// But warning is logged
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Name);
|
||||||
|
}
|
||||||
|
// 6. Case insensitivity for type string
|
||||||
|
{
|
||||||
|
QJsonObject root;
|
||||||
|
QJsonObject sortObj;
|
||||||
|
sortObj["type"] = "DaTe";
|
||||||
|
root["sort"] = sortObj;
|
||||||
|
writeConfig(root);
|
||||||
|
Config config(m_tempDir.path(), {}, m_configPath);
|
||||||
|
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestConfigMgr)
|
||||||
|
#include "tst_configmgr.moc"
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#include <QDate>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include "configmgr.hpp"
|
||||||
|
#include "imagemodel.hpp"
|
||||||
|
#include "imageprovider.hpp"
|
||||||
|
|
||||||
|
class TestImageModel : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase();
|
||||||
|
void testSortName();
|
||||||
|
void testSortDate();
|
||||||
|
void testSortSize();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTemporaryDir m_tempDir;
|
||||||
|
QString m_pathA;
|
||||||
|
QString m_pathB;
|
||||||
|
QString m_pathC;
|
||||||
|
|
||||||
|
void createTestFiles();
|
||||||
|
void waitForModel(ImageModel* model);
|
||||||
|
};
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
// xxd <file> -i
|
||||||
|
static const unsigned char smallGIF[] = {
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0xf0, 0x00,
|
||||||
|
0x00, 0xcd, 0xcf, 0xd2, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||||
|
0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b
|
||||||
|
};
|
||||||
|
static const unsigned char mediumGIF[] = {
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x02, 0x00, 0xf1, 0x00,
|
||||||
|
0x00, 0xb0, 0xb8, 0xc0, 0xb7, 0xbc, 0xc2, 0xd8, 0xdb, 0xda, 0xe2, 0xdd,
|
||||||
|
0xdb, 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x03, 0xd4, 0x10, 0x05,
|
||||||
|
0x00, 0x3b
|
||||||
|
};
|
||||||
|
static const unsigned char largeGIF[] = {
|
||||||
|
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x03, 0x00, 0x03, 0x00, 0xf3, 0x00,
|
||||||
|
0x00, 0x80, 0x8b, 0x9c, 0xa9, 0xad, 0xac, 0xcf, 0xd5, 0xd6, 0xc9, 0xd2,
|
||||||
|
0xdc, 0xde, 0xd7, 0xd8, 0xdf, 0xdf, 0xdf, 0xd3, 0xda, 0xe0, 0xe9, 0xea,
|
||||||
|
0xeb, 0xf8, 0xf0, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x04, 0x07, 0xf0, 0x14, 0x24,
|
||||||
|
0x02, 0x19, 0xc0, 0x44, 0x00, 0x3b
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
void TestImageModel::initTestCase() {
|
||||||
|
createTestFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageModel::createTestFiles() {
|
||||||
|
QVERIFY(m_tempDir.isValid());
|
||||||
|
|
||||||
|
// Create files with specific names, sizes, dates
|
||||||
|
// a.gif: medium size, medium date
|
||||||
|
// c.gif: small size, old date
|
||||||
|
// b.gif: big size, new date
|
||||||
|
// Note: Names are a.gif, b.gif, c.gif for name sort.
|
||||||
|
|
||||||
|
m_pathA = m_tempDir.path() + "/a.gif";
|
||||||
|
m_pathB = m_tempDir.path() + "/b.gif";
|
||||||
|
m_pathC = m_tempDir.path() + "/c.gif";
|
||||||
|
|
||||||
|
{
|
||||||
|
QFile f(m_pathA);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||||
|
f.write(reinterpret_cast<const char*>(mediumGIF), sizeof(mediumGIF));
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QFile f(m_pathB);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||||
|
f.write(reinterpret_cast<const char*>(largeGIF), sizeof(largeGIF));
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QFile f(m_pathC);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||||
|
f.write(reinterpret_cast<const char*>(smallGIF), sizeof(smallGIF));
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set times
|
||||||
|
|
||||||
|
QDateTime now = QDateTime::currentDateTime();
|
||||||
|
QDateTime timeOld = now.addDays(-10);
|
||||||
|
QDateTime timeMid = now.addDays(-5);
|
||||||
|
QDateTime timeNew = now;
|
||||||
|
|
||||||
|
{
|
||||||
|
QFile f(m_pathC);
|
||||||
|
QVERIFY(f.open(QIODevice::ReadWrite));
|
||||||
|
QVERIFY(f.setFileTime(timeOld, QFileDevice::FileModificationTime));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QFile f(m_pathA);
|
||||||
|
QVERIFY(f.open(QIODevice::ReadWrite));
|
||||||
|
QVERIFY(f.setFileTime(timeMid, QFileDevice::FileModificationTime));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QFile f(m_pathB);
|
||||||
|
QVERIFY(f.open(QIODevice::ReadWrite));
|
||||||
|
QVERIFY(f.setFileTime(timeNew, QFileDevice::FileModificationTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageModel::waitForModel(ImageModel* model) {
|
||||||
|
if (!model->isLoading()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QSignalSpy spy(model, &ImageModel::isLoadingChanged);
|
||||||
|
while (model->isLoading()) {
|
||||||
|
if (!spy.wait(5000)) {
|
||||||
|
qWarning() << "Timeout waiting for model to load";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageModel::testSortName() {
|
||||||
|
Config::SortConfigItems sortConfig;
|
||||||
|
sortConfig.type = Config::SortType::Name;
|
||||||
|
sortConfig.reverse = false;
|
||||||
|
|
||||||
|
ImageProvider provider;
|
||||||
|
ImageModel model(provider, sortConfig, QSize(100, 100));
|
||||||
|
|
||||||
|
QStringList paths = {m_pathB, m_pathA, m_pathC}; // Unordered input
|
||||||
|
model.loadAndProcess(paths);
|
||||||
|
waitForModel(&model);
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 3);
|
||||||
|
|
||||||
|
// Expected: a.gif, b.gif, c.gif
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
|
||||||
|
// Reverse
|
||||||
|
sortConfig.reverse = true;
|
||||||
|
model.sortUpdate();
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 3);
|
||||||
|
|
||||||
|
// Expected: c.gif, b.gif, a.gif
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageModel::testSortDate() {
|
||||||
|
Config::SortConfigItems sortConfig;
|
||||||
|
sortConfig.type = Config::SortType::Date;
|
||||||
|
sortConfig.reverse = false;
|
||||||
|
|
||||||
|
ImageProvider provider;
|
||||||
|
ImageModel model(provider, sortConfig, QSize(100, 100));
|
||||||
|
|
||||||
|
QStringList paths = {m_pathA, m_pathC, m_pathB};
|
||||||
|
model.loadAndProcess(paths);
|
||||||
|
waitForModel(&model);
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 3);
|
||||||
|
|
||||||
|
// Expected: c (old), a (mid), b (new)
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
|
||||||
|
// Reverse (Newest first)
|
||||||
|
sortConfig.reverse = true;
|
||||||
|
model.sortUpdate();
|
||||||
|
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImageModel::testSortSize() {
|
||||||
|
Config::SortConfigItems sortConfig;
|
||||||
|
sortConfig.type = Config::SortType::Size;
|
||||||
|
sortConfig.reverse = false;
|
||||||
|
|
||||||
|
ImageProvider provider;
|
||||||
|
ImageModel model(provider, sortConfig, QSize(100, 100));
|
||||||
|
|
||||||
|
QStringList paths = {m_pathB, m_pathC, m_pathA};
|
||||||
|
model.loadAndProcess(paths);
|
||||||
|
waitForModel(&model);
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 3);
|
||||||
|
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
|
||||||
|
// Reverse
|
||||||
|
sortConfig.reverse = true;
|
||||||
|
model.sortUpdate();
|
||||||
|
|
||||||
|
QCOMPARE(model.data(model.index(0), ImageModel::NameRole).toString(), "b.gif");
|
||||||
|
QCOMPARE(model.data(model.index(1), ImageModel::NameRole).toString(), "a.gif");
|
||||||
|
QCOMPARE(model.data(model.index(2), ImageModel::NameRole).toString(), "c.gif");
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestImageModel)
|
||||||
|
#include "tst_imagemodel.moc"
|
||||||
@@ -8,6 +8,9 @@ qt_add_qml_module(${CORELIB_NAME}
|
|||||||
configmgr.hpp configmgr.cpp
|
configmgr.hpp configmgr.cpp
|
||||||
imagemodel.hpp imagemodel.cpp
|
imagemodel.hpp imagemodel.cpp
|
||||||
imageprovider.hpp imageprovider.cpp
|
imageprovider.hpp imageprovider.cpp
|
||||||
|
wallpaperservice.hpp wallpaperservice.cpp
|
||||||
|
palette/data.hpp
|
||||||
|
palette/manager.hpp palette/manager.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(${CORELIB_NAME} PRIVATE
|
target_link_libraries(${CORELIB_NAME} PRIVATE
|
||||||
|
|||||||
+215
-83
@@ -28,7 +28,9 @@ Config::Config(
|
|||||||
}
|
}
|
||||||
if (!searchDirs.isEmpty()) {
|
if (!searchDirs.isEmpty()) {
|
||||||
info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
|
info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
|
||||||
m_wallpaperConfig.dirs.append(searchDirs);
|
for (const auto& dir : searchDirs) {
|
||||||
|
m_wallpaperConfig.dirs.append({dir, false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug("Loading wallpapers ...");
|
debug("Loading wallpapers ...");
|
||||||
@@ -56,70 +58,207 @@ void Config::_loadConfig(const QString& configPath) {
|
|||||||
|
|
||||||
const auto jsonObj = jsonDoc.object();
|
const auto jsonObj = jsonDoc.object();
|
||||||
|
|
||||||
struct ConfigMapping {
|
_loadWallpaperConfig(jsonObj);
|
||||||
QString path;
|
_loadPaletteConfig(jsonObj);
|
||||||
QString key;
|
_loadActionConfig(jsonObj);
|
||||||
std::function<void(const QJsonValue&)> parser;
|
_loadStyleConfig(jsonObj);
|
||||||
};
|
_loadSortConfig(jsonObj);
|
||||||
|
}
|
||||||
|
|
||||||
static const auto parseJsonArray = [](const QJsonValue& val, QStringList& list) {
|
void Config::_loadWallpaperConfig(const QJsonObject& root) {
|
||||||
if (val.isArray()) {
|
if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) {
|
||||||
for (const auto& item : val.toArray()) {
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject& config = root["wallpaper"].toObject();
|
||||||
|
|
||||||
|
if (config.contains("paths") && config["paths"].isArray()) {
|
||||||
|
for (const auto& item : config["paths"].toArray()) {
|
||||||
if (item.isString()) {
|
if (item.isString()) {
|
||||||
list.append(::expandPath(item.toString()));
|
m_wallpaperConfig.paths.append(::expandPath(item.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<ConfigMapping>
|
if (config.contains("dirs") && config["dirs"].isArray()) {
|
||||||
mappings = {
|
for (const auto& item : config["dirs"].toArray()) {
|
||||||
{"wallpaper.paths", "paths", [this](const QJsonValue& val) {
|
if (item.isObject()) {
|
||||||
parseJsonArray(val, m_wallpaperConfig.paths);
|
QJsonObject obj = item.toObject();
|
||||||
}},
|
if (obj.contains("path") && obj["path"].isString()) {
|
||||||
{"wallpaper.dirs", "dirs", [this](const QJsonValue& val) {
|
WallpaperConfigItems::WallpaperDirConfigItem dirConfig;
|
||||||
parseJsonArray(val, m_wallpaperConfig.dirs);
|
dirConfig.path = ::expandPath(obj["path"].toString());
|
||||||
}},
|
if (obj.contains("recursive") && obj["recursive"].isBool()) {
|
||||||
{"wallpaper.excludes", "excludes", [this](const QJsonValue& val) {
|
dirConfig.recursive = obj["recursive"].toBool();
|
||||||
parseJsonArray(val, m_wallpaperConfig.excludes);
|
} else {
|
||||||
}},
|
dirConfig.recursive = false;
|
||||||
{"action.confirm", "confirm", [this](const QJsonValue& val) {
|
}
|
||||||
|
m_wallpaperConfig.dirs.append(dirConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.contains("excludes") && config["excludes"].isArray()) {
|
||||||
|
for (const auto& item : config["excludes"].toArray()) {
|
||||||
|
if (item.isString()) {
|
||||||
|
auto regex = QRegularExpression(item.toString());
|
||||||
|
if (!regex.isValid()) {
|
||||||
|
warn(QString("Invalid regular expression in config: %1").arg(item.toString()));
|
||||||
|
} else {
|
||||||
|
m_wallpaperConfig.excludes.append(regex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::_loadPaletteConfig(const QJsonObject& root) {
|
||||||
|
if (!root.contains("palettes") || !root["palettes"].isArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonArray& palettes = root["palettes"].toArray();
|
||||||
|
|
||||||
|
for (const auto& palItem : palettes) {
|
||||||
|
if (palItem.isObject()) {
|
||||||
|
QJsonObject palObj = palItem.toObject();
|
||||||
|
PaletteConfigItems::PaletteConfigItem palette;
|
||||||
|
if (palObj.contains("name") && palObj["name"].isString()) {
|
||||||
|
palette.name = palObj["name"].toString();
|
||||||
|
}
|
||||||
|
if (palObj.contains("colors") && palObj["colors"].isArray()) {
|
||||||
|
for (const auto& colorItem : palObj["colors"].toArray()) {
|
||||||
|
PaletteConfigItems::PaletteColorConfigItem colorConfig;
|
||||||
|
if (colorItem.isObject()) {
|
||||||
|
QJsonObject colorObj = colorItem.toObject();
|
||||||
|
if (colorObj.contains("name") && colorObj["name"].isString()) {
|
||||||
|
colorConfig.name = colorObj["name"].toString();
|
||||||
|
}
|
||||||
|
if (colorObj.contains("value") && colorObj["value"].isString()) {
|
||||||
|
QColor color(colorObj["value"].toString());
|
||||||
|
if (color.isValid()) {
|
||||||
|
colorConfig.value = color;
|
||||||
|
} else {
|
||||||
|
warn(QString("Invalid color string in config: %1").arg(colorObj["value"].toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (colorItem.isString()) {
|
||||||
|
QColor color(colorItem.toString());
|
||||||
|
if (color.isValid()) {
|
||||||
|
colorConfig.value = color;
|
||||||
|
} else {
|
||||||
|
warn(QString("Invalid color string in config: %1").arg(colorItem.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (colorConfig.value.isValid()) {
|
||||||
|
palette.colors.append(colorConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_paletteConfig.palettes.append(palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::_loadActionConfig(const QJsonObject& root) {
|
||||||
|
if (!root.contains("action") || !root["action"].isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject& config = root["action"].toObject();
|
||||||
|
|
||||||
|
if (config.contains("previewDebounceTime")) {
|
||||||
|
const auto& val = config["previewDebounceTime"];
|
||||||
|
if (val.isDouble() && val.toDouble() >= 0) {
|
||||||
|
m_actionConfig.previewDebounceTime = val.toInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.contains("printSelected")) {
|
||||||
|
const auto& val = config["printSelected"];
|
||||||
|
if (val.isBool()) {
|
||||||
|
m_actionConfig.printSelected = val.toBool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.contains("printPreview")) {
|
||||||
|
const auto& val = config["printPreview"];
|
||||||
|
if (val.isBool()) {
|
||||||
|
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("onRestore")) {
|
||||||
|
const auto& val = config["onRestore"];
|
||||||
if (val.isString()) {
|
if (val.isString()) {
|
||||||
m_actionConfig.confirm = ::expandPath(val.toString());
|
m_actionConfig.onRestore = val.toString();
|
||||||
debug(QString("Action confirm: %1").arg(m_actionConfig.confirm));
|
|
||||||
}
|
}
|
||||||
}},
|
|
||||||
{"style.aspect_ratio", "aspect_ratio", [this](const QJsonValue& val) {
|
|
||||||
if (val.isDouble() && val.toDouble() > 0) {
|
|
||||||
m_styleConfig.aspectRatio = val.toDouble();
|
|
||||||
debug(QString("Aspect ratio: %1").arg(m_styleConfig.aspectRatio));
|
|
||||||
}
|
}
|
||||||
}},
|
if (config.contains("onSelected")) {
|
||||||
{"style.image_width", "image_width", [this](const QJsonValue& val) {
|
const auto& val = config["onSelected"];
|
||||||
|
if (val.isString()) {
|
||||||
|
m_actionConfig.onSelected = val.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.contains("onPreview")) {
|
||||||
|
const auto& val = config["onPreview"];
|
||||||
|
if (val.isString()) {
|
||||||
|
m_actionConfig.onPreview = val.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Config::_loadStyleConfig(const QJsonObject& root) {
|
||||||
|
if (!root.contains("style") || !root["style"].isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject& config = root["style"].toObject();
|
||||||
|
|
||||||
|
if (config.contains("image_width")) {
|
||||||
|
const auto& val = config["image_width"];
|
||||||
if (val.isDouble() && val.toDouble() > 0) {
|
if (val.isDouble() && val.toDouble() > 0) {
|
||||||
m_styleConfig.imageWidth = val.toInt();
|
m_styleConfig.imageWidth = val.toInt();
|
||||||
debug(QString("Image width: %1").arg(m_styleConfig.imageWidth));
|
|
||||||
}
|
}
|
||||||
}},
|
}
|
||||||
{"style.image_focus_width", "image_focus_width", [this](const QJsonValue& val) {
|
if (config.contains("image_height")) {
|
||||||
|
const auto& val = config["image_height"];
|
||||||
if (val.isDouble() && val.toDouble() > 0) {
|
if (val.isDouble() && val.toDouble() > 0) {
|
||||||
m_styleConfig.imageFocusWidth = val.toInt();
|
m_styleConfig.imageHeight = val.toInt();
|
||||||
debug(QString("Image focus width: %1").arg(m_styleConfig.imageFocusWidth));
|
|
||||||
}
|
}
|
||||||
}},
|
}
|
||||||
{"style.window_width", "window_width", [this](const QJsonValue& val) {
|
if (config.contains("image_focus_scale")) {
|
||||||
|
const auto& val = config["image_focus_scale"];
|
||||||
|
if (val.isDouble() && val.toDouble() > 0) {
|
||||||
|
m_styleConfig.imageFocusScale = val.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.contains("window_width")) {
|
||||||
|
const auto& val = config["window_width"];
|
||||||
if (val.isDouble() && val.toDouble() > 0) {
|
if (val.isDouble() && val.toDouble() > 0) {
|
||||||
m_styleConfig.windowWidth = val.toInt();
|
m_styleConfig.windowWidth = val.toInt();
|
||||||
debug(QString("Window width: %1").arg(m_styleConfig.windowWidth));
|
|
||||||
}
|
}
|
||||||
}},
|
}
|
||||||
{"style.window_height", "window_height", [this](const QJsonValue& val) {
|
if (config.contains("window_height")) {
|
||||||
|
const auto& val = config["window_height"];
|
||||||
if (val.isDouble() && val.toDouble() > 0) {
|
if (val.isDouble() && val.toDouble() > 0) {
|
||||||
m_styleConfig.windowHeight = val.toInt();
|
m_styleConfig.windowHeight = val.toInt();
|
||||||
debug(QString("Window height: %1").arg(m_styleConfig.windowHeight));
|
|
||||||
}
|
}
|
||||||
}},
|
}
|
||||||
{"sort.type", "type", [this](const QJsonValue& val) {
|
}
|
||||||
|
|
||||||
|
void Config::_loadSortConfig(const QJsonObject& root) {
|
||||||
|
if (!root.contains("sort") || !root["sort"].isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject& config = root["sort"].toObject();
|
||||||
|
|
||||||
|
if (config.contains("type")) {
|
||||||
|
const auto& val = config["type"];
|
||||||
if (val.isString()) {
|
if (val.isString()) {
|
||||||
QString type = val.toString().toLower();
|
QString type = val.toString().toLower();
|
||||||
if (type == "none") {
|
if (type == "none") {
|
||||||
@@ -134,39 +273,12 @@ void Config::_loadConfig(const QString& configPath) {
|
|||||||
warn(QString("Unknown sort type: %1").arg(type));
|
warn(QString("Unknown sort type: %1").arg(type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug(QString("Sort type: %1").arg(static_cast<int>(m_sortConfig.type)));
|
}
|
||||||
}},
|
if (config.contains("reverse")) {
|
||||||
{"sort.reverse", "reverse", [this](const QJsonValue& val) {
|
const auto& val = config["reverse"];
|
||||||
if (val.isBool()) {
|
if (val.isBool()) {
|
||||||
m_sortConfig.reverse = val.toBool();
|
m_sortConfig.reverse = val.toBool();
|
||||||
debug(QString("Sort reverse: %1").arg(m_sortConfig.reverse));
|
|
||||||
}
|
}
|
||||||
}},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& mapping : mappings) {
|
|
||||||
([&mapping, &jsonObj]() {
|
|
||||||
auto pathParts = mapping.path.split('.');
|
|
||||||
|
|
||||||
QJsonObject currentObj = jsonObj;
|
|
||||||
QJsonValue targetValue;
|
|
||||||
|
|
||||||
for (int i = 0; i < pathParts.size() - 1; ++i) {
|
|
||||||
if (currentObj.contains(pathParts[i]) && currentObj[pathParts[i]].isObject()) {
|
|
||||||
currentObj = currentObj[pathParts[i]].toObject();
|
|
||||||
} else {
|
|
||||||
debug(QString("Path '%1' not found").arg(pathParts.mid(0, i + 1).join('.')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString& finalKey = pathParts.last();
|
|
||||||
if (currentObj.contains(finalKey)) {
|
|
||||||
mapping.parser(currentObj[finalKey]);
|
|
||||||
} else {
|
|
||||||
debug(QString("Key '%1' not found in '%2'").arg(finalKey).arg(mapping.path));
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,22 +293,42 @@ void Config::_loadWallpapers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size()));
|
debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size()));
|
||||||
for (const QString& dirPath : std::as_const(m_wallpaperConfig.dirs)) {
|
for (const auto& dirConfig : std::as_const(m_wallpaperConfig.dirs)) {
|
||||||
QDir dir(dirPath);
|
if (checkDir(dirConfig.path)) {
|
||||||
if (checkDir(dirPath)) {
|
std::function<void(const QDir&)> scanDir;
|
||||||
QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
|
scanDir = [&](const QDir& d) {
|
||||||
|
QStringList files = d.entryList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
for (const QString& file : std::as_const(files)) {
|
for (const QString& file : std::as_const(files)) {
|
||||||
QString filePath = dir.filePath(file);
|
QString filePath = d.filePath(file);
|
||||||
paths.insert(expandPath(filePath));
|
paths.insert(expandPath(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dirConfig.recursive) {
|
||||||
|
QStringList subDirs = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
debug(QString("Scanning directory '%1' for subdirectories... Found %2").arg(d.absolutePath()).arg(subDirs.size()));
|
||||||
|
for (const QString& subDir : std::as_const(subDirs)) {
|
||||||
|
scanDir(QDir(d.filePath(subDir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
scanDir(QDir(dirConfig.path));
|
||||||
} else {
|
} else {
|
||||||
warn(QString("Directory '%1' does not exist").arg(dirPath));
|
warn(QString("Directory '%1' does not exist").arg(dirConfig.path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
|
debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
|
||||||
for (const QString& exclude : std::as_const(m_wallpaperConfig.excludes)) {
|
QStringList toRemove;
|
||||||
paths.remove(exclude);
|
for (const auto& exclude : std::as_const(m_wallpaperConfig.excludes)) {
|
||||||
|
for (const QString& path : std::as_const(paths)) {
|
||||||
|
if (exclude.match(path).hasMatch()) {
|
||||||
|
toRemove.append(path);
|
||||||
|
debug(QString("Excluded path '%1' matched by regex '%2'").arg(path).arg(exclude.pattern()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& path : toRemove) {
|
||||||
|
paths.remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_wallpapers.reserve(paths.size());
|
m_wallpapers.reserve(paths.size());
|
||||||
|
|||||||
+84
-34
@@ -1,37 +1,55 @@
|
|||||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||||
#define WALLREEL_CONFIGMGR_HPP
|
#define WALLREEL_CONFIGMGR_HPP
|
||||||
|
|
||||||
|
#include <qregularexpression.h>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QQmlEngine>
|
#include <QRegularExpression>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
// Config entries:
|
// Config entries:
|
||||||
//
|
//
|
||||||
// wallpaper.paths array image paths
|
// wallpaper.paths array [] List of paths to images.
|
||||||
// wallpaper.dirs array directories to search for images.
|
// wallpaper.dirs array [] Directories to search for images.
|
||||||
// all images in these directories will be added.
|
// wallpaper.dirs[].path string "" Path to the directory.
|
||||||
// NOT recursive.
|
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
||||||
// wallpaper.excludes array exclude patterns
|
// wallpaper.excludes array [] Exclude patterns (regex)
|
||||||
//
|
//
|
||||||
// action.confirm string command to execute on confirm
|
// 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
|
||||||
//
|
//
|
||||||
// style.aspect_ratio number (width / height) of each image
|
// action.previewDebounceTime number 300 Minimum debounce time for preview action in milliseconds
|
||||||
// style.image_width number width of each image
|
// action.printSelected boolean false Whether to print the selected wallpaper path to stdout on confirm
|
||||||
// style.image_focus_width number width of focused image
|
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
|
||||||
// style.window_width number fixed window width
|
// action.saveState object {} Key-value pairs to save the state, useful for restore command
|
||||||
// style.window_height number fixed window height
|
// 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)
|
||||||
//
|
//
|
||||||
// sort.type string sorting type: "none", "name", "date", "size"
|
// style.image_width number 320 Width of each image
|
||||||
// sort.reverse boolean whether to reverse the sorting order
|
// 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
|
||||||
|
|
||||||
class Config : public QObject {
|
class Config : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
Q_PROPERTY(double aspectRatio READ getAspectRatio CONSTANT)
|
|
||||||
Q_PROPERTY(int imageWidth READ getImageWidth CONSTANT)
|
Q_PROPERTY(int imageWidth READ getImageWidth CONSTANT)
|
||||||
Q_PROPERTY(int imageFocusWidth READ getImageFocusWidth CONSTANT)
|
Q_PROPERTY(int imageHeight READ getImageHeight CONSTANT)
|
||||||
|
Q_PROPERTY(double imageFocusScale READ getImageFocusScale CONSTANT)
|
||||||
Q_PROPERTY(int windowWidth READ getWindowWidth CONSTANT)
|
Q_PROPERTY(int windowWidth READ getWindowWidth CONSTANT)
|
||||||
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
|
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
|
||||||
|
|
||||||
@@ -44,30 +62,56 @@ class Config : public QObject {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct WallpaperConfigItems {
|
struct WallpaperConfigItems {
|
||||||
QStringList paths; // "wallpaper.paths"
|
struct WallpaperDirConfigItem {
|
||||||
QStringList dirs; // "wallpaper.dirs"
|
QString path;
|
||||||
QStringList excludes; // "wallpaper.excludes"
|
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 ActionConfigItems {
|
||||||
QString confirm; // "action.confirm"
|
|
||||||
|
QHash<QString, QString> saveState;
|
||||||
|
QString onSelected;
|
||||||
|
QString onPreview;
|
||||||
|
QString onRestore;
|
||||||
|
int previewDebounceTime = 300; // milliseconds
|
||||||
|
bool printSelected = false;
|
||||||
|
bool printPreview = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct StyleConfigItems {
|
struct StyleConfigItems {
|
||||||
double aspectRatio = 1.6; // "style.aspect_ratio"
|
double imageFocusScale = 1.5;
|
||||||
int imageWidth = 320; // "style.image_width"
|
int imageWidth = 320;
|
||||||
int imageFocusWidth = 480; // "style.image_focus_width"
|
int imageHeight = 200;
|
||||||
int windowWidth = 750; // "style.window_width"
|
int windowWidth = 750;
|
||||||
int windowHeight = 500; // "style.window_height"
|
int windowHeight = 500;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SortConfigItems {
|
struct SortConfigItems {
|
||||||
SortType type = SortType::Name; // "sort.type"
|
SortType type = SortType::Name;
|
||||||
bool reverse = false; // "sort.reverse"
|
bool reverse = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
Config(
|
Config(
|
||||||
const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel"
|
const QString& configDir,
|
||||||
const QStringList& searchDirs = {},
|
const QStringList& searchDirs = {},
|
||||||
const QString& configPath = "", // Override the default config path
|
const QString& configPath = "", // Override the default config path
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
@@ -80,26 +124,26 @@ class Config : public QObject {
|
|||||||
|
|
||||||
const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
|
const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
|
||||||
|
|
||||||
|
const PaletteConfigItems& getPaletteConfig() const { return m_paletteConfig; }
|
||||||
|
|
||||||
const ActionConfigItems& getActionConfig() const { return m_actionConfig; }
|
const ActionConfigItems& getActionConfig() const { return m_actionConfig; }
|
||||||
|
|
||||||
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
|
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
|
||||||
|
|
||||||
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
||||||
|
|
||||||
double getAspectRatio() const { return m_styleConfig.aspectRatio; }
|
|
||||||
|
|
||||||
int getImageWidth() const { return m_styleConfig.imageWidth; }
|
int getImageWidth() const { return m_styleConfig.imageWidth; }
|
||||||
|
|
||||||
int getImageFocusWidth() const { return m_styleConfig.imageFocusWidth; }
|
int getImageHeight() const { return m_styleConfig.imageHeight; }
|
||||||
|
|
||||||
|
double getImageFocusScale() const { return m_styleConfig.imageFocusScale; }
|
||||||
|
|
||||||
int getWindowWidth() const { return m_styleConfig.windowWidth; }
|
int getWindowWidth() const { return m_styleConfig.windowWidth; }
|
||||||
|
|
||||||
int getWindowHeight() const { return m_styleConfig.windowHeight; }
|
int getWindowHeight() const { return m_styleConfig.windowHeight; }
|
||||||
|
|
||||||
QSize getFocusImageSize() const {
|
QSize getFocusImageSize() const {
|
||||||
int width = m_styleConfig.imageFocusWidth;
|
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
||||||
int height = static_cast<int>(width / m_styleConfig.aspectRatio);
|
|
||||||
return {width, height};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const QString s_DefaultConfigFileName;
|
static const QString s_DefaultConfigFileName;
|
||||||
@@ -108,9 +152,15 @@ class Config : public QObject {
|
|||||||
private:
|
private:
|
||||||
void _loadConfig(const QString& configPath);
|
void _loadConfig(const QString& configPath);
|
||||||
void _loadWallpapers();
|
void _loadWallpapers();
|
||||||
|
void _loadWallpaperConfig(const QJsonObject& config);
|
||||||
|
void _loadPaletteConfig(const QJsonObject& config);
|
||||||
|
void _loadActionConfig(const QJsonObject& config);
|
||||||
|
void _loadStyleConfig(const QJsonObject& config);
|
||||||
|
void _loadSortConfig(const QJsonObject& config);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
WallpaperConfigItems m_wallpaperConfig;
|
WallpaperConfigItems m_wallpaperConfig;
|
||||||
|
PaletteConfigItems m_paletteConfig;
|
||||||
ActionConfigItems m_actionConfig;
|
ActionConfigItems m_actionConfig;
|
||||||
StyleConfigItems m_styleConfig;
|
StyleConfigItems m_styleConfig;
|
||||||
SortConfigItems m_sortConfig;
|
SortConfigItems m_sortConfig;
|
||||||
|
|||||||
+11
-11
@@ -16,7 +16,7 @@ ImageData* ImageData::create(const QString& path, const QSize& size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImageData::ImageData(const QString& path, const QSize& targetSize)
|
ImageData::ImageData(const QString& path, const QSize& targetSize)
|
||||||
: file(path) {
|
: m_file(path) {
|
||||||
QImageReader reader(path);
|
QImageReader reader(path);
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
warn(QString("Failed to load image from path: %1").arg(path));
|
warn(QString("Failed to load image from path: %1").arg(path));
|
||||||
@@ -38,27 +38,27 @@ ImageData::ImageData(const QString& path, const QSize& targetSize)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reader.read(&image)) {
|
if (!reader.read(&m_image)) {
|
||||||
warn(QString("Failed to load image from path: %1").arg(path));
|
warn(QString("Failed to load image from path: %1").arg(path));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.width() > processSize.width() || image.height() > processSize.height()) {
|
if (m_image.width() > processSize.width() || m_image.height() > processSize.height()) {
|
||||||
image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
m_image = m_image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop to target size if necessary
|
// Crop to target size if necessary
|
||||||
if (image.size() != targetSize) {
|
if (m_image.size() != targetSize) {
|
||||||
int x = (image.width() - targetSize.width()) / 2;
|
int x = (m_image.width() - targetSize.width()) / 2;
|
||||||
int y = (image.height() - targetSize.height()) / 2;
|
int y = (m_image.height() - targetSize.height()) / 2;
|
||||||
image = image.copy(x, y, targetSize.width(), targetSize.height());
|
m_image = m_image.copy(x, y, targetSize.width(), targetSize.height());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to GPU-friendly format
|
// Convert to GPU-friendly format
|
||||||
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
if (m_image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
m_image = m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ID
|
// Create ID
|
||||||
id = QString::number(qHash(file.absoluteFilePath()));
|
m_id = QString::number(qHash(m_file.absoluteFilePath()));
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -5,30 +5,30 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
|
||||||
class ImageData {
|
class ImageData {
|
||||||
QString id;
|
QString m_id;
|
||||||
QFileInfo file;
|
QFileInfo m_file;
|
||||||
QImage image;
|
QImage m_image;
|
||||||
|
|
||||||
ImageData(const QString& path, const QSize& size);
|
ImageData(const QString& path, const QSize& size);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static ImageData* create(const QString& path, const QSize& size);
|
static ImageData* create(const QString& path, const QSize& size);
|
||||||
|
|
||||||
const QImage& getImage() const { return image; }
|
const QImage& getImage() const { return m_image; }
|
||||||
|
|
||||||
const QString& getId() const { return id; }
|
const QString& getId() const { return m_id; }
|
||||||
|
|
||||||
bool isValid() const { return !image.isNull(); }
|
bool isValid() const { return !m_image.isNull(); }
|
||||||
|
|
||||||
QString getFullPath() const { return file.absoluteFilePath(); }
|
QString getFullPath() const { return m_file.absoluteFilePath(); }
|
||||||
|
|
||||||
QString getFileName() const { return file.fileName(); }
|
QString getFileName() const { return m_file.fileName(); }
|
||||||
|
|
||||||
QDateTime getLastModified() const { return file.lastModified(); }
|
QDateTime getLastModified() const { return m_file.lastModified(); }
|
||||||
|
|
||||||
qint64 getSize() const { return file.size(); }
|
qint64 getSize() const { return m_file.size(); }
|
||||||
|
|
||||||
const QFileInfo& getFileInfo() const { return file; }
|
const QFileInfo& getFileInfo() const { return m_file; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include "imagedata.hpp"
|
#include "imagedata.hpp"
|
||||||
|
|
||||||
ImageModel::ImageModel(
|
ImageModel::ImageModel(
|
||||||
ImageProvider* provider,
|
ImageProvider& provider,
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
QSize thumbnailSize,
|
QSize thumbnailSize,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
@@ -19,7 +19,7 @@ ImageModel::ImageModel(
|
|||||||
&m_watcher,
|
&m_watcher,
|
||||||
&QFutureWatcher<ImageData*>::finished,
|
&QFutureWatcher<ImageData*>::finished,
|
||||||
this,
|
this,
|
||||||
&ImageModel::onProcessingFinished);
|
&ImageModel::_onProcessingFinished);
|
||||||
connect(
|
connect(
|
||||||
&m_progressUpdateTimer,
|
&m_progressUpdateTimer,
|
||||||
&QTimer::timeout,
|
&QTimer::timeout,
|
||||||
@@ -33,6 +33,7 @@ ImageModel::~ImageModel() {
|
|||||||
m_watcher.cancel();
|
m_watcher.cancel();
|
||||||
m_watcher.waitForFinished();
|
m_watcher.waitForFinished();
|
||||||
qDeleteAll(m_data);
|
qDeleteAll(m_data);
|
||||||
|
m_data.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ImageModel::rowCount(const QModelIndex& parent) const {
|
int ImageModel::rowCount(const QModelIndex& parent) const {
|
||||||
@@ -67,16 +68,10 @@ void ImageModel::loadAndProcess(const QStringList& paths) {
|
|||||||
m_isLoading = true;
|
m_isLoading = true;
|
||||||
emit isLoadingChanged();
|
emit isLoadingChanged();
|
||||||
|
|
||||||
beginResetModel();
|
_clearData();
|
||||||
if (!m_data.isEmpty()) {
|
|
||||||
qDeleteAll(m_data);
|
|
||||||
}
|
|
||||||
m_data.clear();
|
|
||||||
m_provider->clear();
|
|
||||||
endResetModel();
|
|
||||||
|
|
||||||
m_processedCount = 0;
|
m_processedCount = 0;
|
||||||
m_progressUpdateTimer.start(s_progressUpdateInterval);
|
m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
|
||||||
const auto thumbnailSize = m_thumbnailSize;
|
const auto thumbnailSize = m_thumbnailSize;
|
||||||
const auto counterPtr = &m_processedCount;
|
const auto counterPtr = &m_processedCount;
|
||||||
QFuture<ImageData*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
|
QFuture<ImageData*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
|
||||||
@@ -94,12 +89,12 @@ void ImageModel::stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageModel::onProgressValueChanged(int value) {
|
void ImageModel::_onProgressValueChanged(int value) {
|
||||||
Q_UNUSED(value);
|
Q_UNUSED(value);
|
||||||
emit progressChanged();
|
emit progressChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImageModel::onProcessingFinished() {
|
void ImageModel::_onProcessingFinished() {
|
||||||
auto results = m_watcher.future().results();
|
auto results = m_watcher.future().results();
|
||||||
for (auto& data : results) {
|
for (auto& data : results) {
|
||||||
if (data && data->isValid()) {
|
if (data && data->isValid()) {
|
||||||
@@ -116,7 +111,7 @@ void ImageModel::onProcessingFinished() {
|
|||||||
m_progressUpdateTimer.stop();
|
m_progressUpdateTimer.stop();
|
||||||
emit progressChanged();
|
emit progressChanged();
|
||||||
// emit isLoadingChanged();
|
// emit isLoadingChanged();
|
||||||
QTimer::singleShot(s_isLoadingUpdateInterval, this, [this]() {
|
QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() {
|
||||||
emit isLoadingChanged();
|
emit isLoadingChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,27 +123,29 @@ void ImageModel::sortUpdate() {
|
|||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool result = false;
|
if (a == b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageData* first = reverse ? b : a;
|
||||||
|
ImageData* second = reverse ? a : b;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Config::SortType::Name:
|
case Config::SortType::Name:
|
||||||
result = QString::compare(a->getFileName(), b->getFileName(), Qt::CaseInsensitive) < 0;
|
return QString::compare(first->getFileName(), second->getFileName(), Qt::CaseInsensitive) < 0;
|
||||||
break;
|
|
||||||
case Config::SortType::Date:
|
case Config::SortType::Date:
|
||||||
result = a->getLastModified() < b->getLastModified();
|
return first->getLastModified() < second->getLastModified();
|
||||||
break;
|
|
||||||
case Config::SortType::Size:
|
case Config::SortType::Size:
|
||||||
result = a->getSize() < b->getSize();
|
return first->getSize() < second->getSize();
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
return false;
|
||||||
}
|
}
|
||||||
return reverse ? !result : result;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_provider->clear();
|
m_provider.clear();
|
||||||
for (const auto& item : m_data) {
|
for (const auto& item : m_data) {
|
||||||
m_provider->insert(item);
|
m_provider.insert(item);
|
||||||
}
|
}
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
@@ -169,3 +166,31 @@ QVariant ImageModel::dataAt(int index, const QString& roleName) const {
|
|||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImageModel::_clearData() {
|
||||||
|
beginResetModel();
|
||||||
|
m_provider.clear();
|
||||||
|
qDeleteAll(m_data);
|
||||||
|
m_data.clear();
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageModel::selectImage(int index) {
|
||||||
|
if (index < 0 || index >= m_data.count()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& item = m_data[index];
|
||||||
|
if (item) {
|
||||||
|
emit imageSelected(*item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageModel::previewImage(int index) {
|
||||||
|
if (index < 0 || index >= m_data.count()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& item = m_data[index];
|
||||||
|
if (item) {
|
||||||
|
emit imagePreviewed(*item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ImageModel : public QAbstractListModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImageModel(
|
ImageModel(
|
||||||
ImageProvider* provider,
|
ImageProvider& provider,
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
QSize thumbnailSize,
|
QSize thumbnailSize,
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
@@ -57,30 +57,38 @@ class ImageModel : public QAbstractListModel {
|
|||||||
|
|
||||||
Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const;
|
Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void selectImage(int index);
|
||||||
|
|
||||||
|
Q_INVOKABLE void previewImage(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void _clearData();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void isLoadingChanged();
|
void isLoadingChanged();
|
||||||
void progressChanged();
|
void progressChanged();
|
||||||
void totalCountChanged();
|
void totalCountChanged();
|
||||||
void imageSelected(const QString& path);
|
void imageSelected(const ImageData& imageData);
|
||||||
|
void imagePreviewed(const ImageData& imageData);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onProgressValueChanged(int value);
|
void _onProgressValueChanged(int value);
|
||||||
void onProcessingFinished();
|
void _onProcessingFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ImageProvider* m_provider;
|
ImageProvider& m_provider;
|
||||||
const Config::SortConfigItems& m_sortConfig;
|
const Config::SortConfigItems& m_sortConfig;
|
||||||
QSize m_thumbnailSize;
|
QSize m_thumbnailSize;
|
||||||
|
|
||||||
QList<ImageData*> m_data;
|
QVector<ImageData*> m_data;
|
||||||
|
|
||||||
QFutureWatcher<ImageData*> m_watcher;
|
QFutureWatcher<ImageData*> m_watcher;
|
||||||
bool m_isLoading = false;
|
bool m_isLoading = false;
|
||||||
|
|
||||||
std::atomic<int> m_processedCount{0};
|
std::atomic<int> m_processedCount{0};
|
||||||
QTimer m_progressUpdateTimer;
|
QTimer m_progressUpdateTimer;
|
||||||
static constexpr int s_progressUpdateInterval = 30;
|
static constexpr int s_ProgressUpdateIntervalMs = 30;
|
||||||
static constexpr int s_isLoadingUpdateInterval = 50;
|
static constexpr int s_IsLoadingUpdateIntervalMs = 50;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // WALLREEL_IMAGEMODEL_HPP
|
#endif // WALLREEL_IMAGEMODEL_HPP
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef WALLREEL_PALETTE_DATA_HPP
|
||||||
|
#define WALLREEL_PALETTE_DATA_HPP
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
struct ColorItem {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name MEMBER name CONSTANT)
|
||||||
|
Q_PROPERTY(QColor color MEMBER color CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QColor color;
|
||||||
|
|
||||||
|
bool operator==(const ColorItem& other) const {
|
||||||
|
return name == other.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PaletteItem {
|
||||||
|
Q_GADGET
|
||||||
|
Q_PROPERTY(QString name MEMBER name CONSTANT)
|
||||||
|
Q_PROPERTY(QList<ColorItem> colors MEMBER colors CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QList<ColorItem> colors;
|
||||||
|
|
||||||
|
Q_INVOKABLE QColor getColor(const QString& colorName) const {
|
||||||
|
for (const auto& entry : colors) {
|
||||||
|
if (entry.name == colorName) return entry.color;
|
||||||
|
}
|
||||||
|
return QColor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(ColorItem)
|
||||||
|
Q_DECLARE_METATYPE(PaletteItem)
|
||||||
|
|
||||||
|
#endif // WALLREEL_PALETTE_DATA_HPP
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#include "manager.hpp"
|
||||||
|
|
||||||
|
#include "predefined.hpp"
|
||||||
|
|
||||||
|
PaletteManager::PaletteManager(
|
||||||
|
const Config::PaletteConfigItems& config,
|
||||||
|
QObject* parent) : QObject(parent) {
|
||||||
|
// The new ones overrides the old ones, use a hashtable to track
|
||||||
|
// the latest index of each palette name, then only insert the
|
||||||
|
// ones whose index matches the latest index in the hashtable
|
||||||
|
QHash<QString, int> lastSeen;
|
||||||
|
lastSeen.reserve(preDefinedPalettes.size() + config.palettes.size());
|
||||||
|
|
||||||
|
for (int i = 0; i < preDefinedPalettes.size(); i++) {
|
||||||
|
lastSeen[preDefinedPalettes[i].name] = i;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < config.palettes.size(); i++) {
|
||||||
|
lastSeen[config.palettes[i].name] = preDefinedPalettes.size() + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_palettes.reserve(lastSeen.size());
|
||||||
|
for (int i = 0; i < preDefinedPalettes.size(); i++) {
|
||||||
|
const auto& p = preDefinedPalettes[i];
|
||||||
|
if (lastSeen[p.name] == i) {
|
||||||
|
m_palettes.append({p.name, p.colors});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < config.palettes.size(); i++) {
|
||||||
|
const auto& p = config.palettes[i];
|
||||||
|
if (lastSeen[p.name] == preDefinedPalettes.size() + i) {
|
||||||
|
auto newP = PaletteItem{p.name, {}};
|
||||||
|
newP.colors.reserve(p.colors.size());
|
||||||
|
for (const auto& c : p.colors) {
|
||||||
|
if (!c.value.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
newP.colors.append({c.name, c.value});
|
||||||
|
}
|
||||||
|
m_palettes.append(newP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef WALLREEL_PALETTE_MANAGER_HPP
|
||||||
|
#define WALLREEL_PALETTE_MANAGER_HPP
|
||||||
|
|
||||||
|
#include "../configmgr.hpp"
|
||||||
|
#include "data.hpp"
|
||||||
|
|
||||||
|
class PaletteManager : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QList<PaletteItem> availablePalettes READ availablePalettes CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
PaletteManager(const Config::PaletteConfigItems& config,
|
||||||
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
|
const QList<PaletteItem>& availablePalettes() const {
|
||||||
|
return m_palettes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_INVOKABLE PaletteItem getPalette(const QString& name) const {
|
||||||
|
for (const auto& p : m_palettes) {
|
||||||
|
if (p.name == name) return p;
|
||||||
|
}
|
||||||
|
return PaletteItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<PaletteItem> m_palettes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WALLREEL_PALETTE_MANAGER_HPP
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#ifndef WALLREEL_PALETTES_PREDEFINED_HPP
|
||||||
|
#define WALLREEL_PALETTES_PREDEFINED_HPP
|
||||||
|
|
||||||
|
#include "data.hpp"
|
||||||
|
|
||||||
|
inline const QList<PaletteItem> preDefinedPalettes = {
|
||||||
|
|
||||||
|
{
|
||||||
|
.name = "Catppuccin Latte",
|
||||||
|
.colors = {
|
||||||
|
|
||||||
|
{"rosewater", "#dc8a78"},
|
||||||
|
{"flamingo", "#dd7878"},
|
||||||
|
{"pink", "#ea76cb"},
|
||||||
|
{"mauve", "#8839ef"},
|
||||||
|
{"red", "#d20f39"},
|
||||||
|
{"maroon", "#e64553"},
|
||||||
|
{"peach", "#fe640b"},
|
||||||
|
{"yellow", "#df8e1d"},
|
||||||
|
{"green", "#40a02b"},
|
||||||
|
{"teal", "#179299"},
|
||||||
|
{"sky", "#04a5e5"},
|
||||||
|
{"sapphire", "#209fb5"},
|
||||||
|
{"blue", "#1e66f5"},
|
||||||
|
{"lavender", "#7287fd"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Catppuccin Frappe",
|
||||||
|
.colors = {
|
||||||
|
{"rosewater", "#f2d5cf"},
|
||||||
|
{"flamingo", "#eebebe"},
|
||||||
|
{"pink", "#f4b8e4"},
|
||||||
|
{"mauve", "#ca9ee6"},
|
||||||
|
{"red", "#e78284"},
|
||||||
|
{"maroon", "#ea999c"},
|
||||||
|
{"peach", "#ef9f76"},
|
||||||
|
{"yellow", "#e5c890"},
|
||||||
|
{"green", "#a6d189"},
|
||||||
|
{"teal", "#81c8be"},
|
||||||
|
{"sky", "#99d1db"},
|
||||||
|
{"sapphire", "#85c1dc"},
|
||||||
|
{"blue", "#8caaee"},
|
||||||
|
{"lavender", "#babbf1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Catppuccin Macchiato",
|
||||||
|
.colors = {
|
||||||
|
{"rosewater", "#f4dbd6"},
|
||||||
|
{"flamingo", "#f0c6c6"},
|
||||||
|
{"pink", "#f5bde6"},
|
||||||
|
{"mauve", "#c6a0f6"},
|
||||||
|
{"red", "#ed8796"},
|
||||||
|
{"maroon", "#ee99a0"},
|
||||||
|
{"peach", "#f5a97f"},
|
||||||
|
{"yellow", "#eed49f"},
|
||||||
|
{"green", "#a6da95"},
|
||||||
|
{"teal", "#8bd5ca"},
|
||||||
|
{"sky", "#91d7e3"},
|
||||||
|
{"sapphire", "#7dc4e4"},
|
||||||
|
{"blue", "#8aadf4"},
|
||||||
|
{"lavender", "#b7bdf8"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Catppuccin Mocha",
|
||||||
|
.colors = {
|
||||||
|
|
||||||
|
{"rosewater", "#f5e0dc"},
|
||||||
|
{"flamingo", "#f2cdcd"},
|
||||||
|
{"pink", "#f5c2e7"},
|
||||||
|
{"mauve", "#cba6f7"},
|
||||||
|
{"red", "#f38ba8"},
|
||||||
|
{"maroon", "#eba0ac"},
|
||||||
|
{"peach", "#fab387"},
|
||||||
|
{"yellow", "#f9e2af"},
|
||||||
|
{"green", "#a6e3a1"},
|
||||||
|
{"teal", "#94e2d5"},
|
||||||
|
{"sky", "#89dceb"},
|
||||||
|
{"sapphire", "#74c7ec"},
|
||||||
|
{"blue", "#89b4fa"},
|
||||||
|
{"lavender", "#b4befe"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WALLREEL_PALETTES_PREDEFINED_HPP
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#ifndef TEXTTEMPLATE_HPP
|
||||||
|
#define TEXTTEMPLATE_HPP
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Replaces {{ key }} style placeholders in a template string with corresponding values from a map.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Whitespace tolerance: {{ key }}, {{key}}, {{ key }} are all valid.
|
||||||
|
* - Missing keys are left as-is (no replacement).
|
||||||
|
* - Nested braces or malformed placeholders are ignored.
|
||||||
|
* - Empty keys are ignored.
|
||||||
|
* - Keys are trimmed before lookup.
|
||||||
|
*
|
||||||
|
* @param templateStr The template string containing {{ key }} placeholders.
|
||||||
|
* @param variables A map of key-value pairs for substitution.
|
||||||
|
* @return The rendered string with placeholders replaced.
|
||||||
|
*/
|
||||||
|
inline QString renderTemplate(const QString& templateStr, const QMap<QString, QString>& variables) {
|
||||||
|
if (templateStr.isEmpty() || variables.isEmpty()) {
|
||||||
|
return templateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match {{ ... }} with possible whitespace around the key.
|
||||||
|
// Use a non-greedy match for the content inside braces to handle multiple placeholders correctly.
|
||||||
|
static const QRegularExpression regex(
|
||||||
|
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"),
|
||||||
|
QRegularExpression::DontCaptureOption);
|
||||||
|
|
||||||
|
// We need the capture group, so rebuild without DontCaptureOption
|
||||||
|
static const QRegularExpression placeholderRegex(
|
||||||
|
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
|
||||||
|
|
||||||
|
QString result;
|
||||||
|
result.reserve(templateStr.size());
|
||||||
|
|
||||||
|
qsizetype lastPos = 0;
|
||||||
|
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QRegularExpressionMatch match = it.next();
|
||||||
|
const qsizetype matchStart = match.capturedStart(0);
|
||||||
|
const qsizetype matchLength = match.capturedLength(0);
|
||||||
|
const QString key = match.captured(1).trimmed();
|
||||||
|
|
||||||
|
// Append everything before this match
|
||||||
|
result.append(templateStr.mid(lastPos, matchStart - lastPos));
|
||||||
|
|
||||||
|
if (!key.isEmpty() && variables.contains(key)) {
|
||||||
|
// Replace with the value from the map
|
||||||
|
result.append(variables.value(key));
|
||||||
|
} else {
|
||||||
|
// Key not found or empty — leave the placeholder as-is
|
||||||
|
result.append(match.captured(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPos = matchStart + matchLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append any remaining text after the last match
|
||||||
|
if (lastPos < templateStr.size()) {
|
||||||
|
result.append(templateStr.mid(lastPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Overload accepting QHash for convenience.
|
||||||
|
*/
|
||||||
|
inline QString renderTemplate(const QString& templateStr, const QHash<QString, QString>& variables) {
|
||||||
|
if (templateStr.isEmpty() || variables.isEmpty()) {
|
||||||
|
return templateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const QRegularExpression placeholderRegex(
|
||||||
|
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
|
||||||
|
|
||||||
|
QString result;
|
||||||
|
result.reserve(templateStr.size());
|
||||||
|
|
||||||
|
qsizetype lastPos = 0;
|
||||||
|
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
|
||||||
|
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QRegularExpressionMatch match = it.next();
|
||||||
|
const qsizetype matchStart = match.capturedStart(0);
|
||||||
|
const qsizetype matchLength = match.capturedLength(0);
|
||||||
|
const QString key = match.captured(1).trimmed();
|
||||||
|
|
||||||
|
result.append(templateStr.mid(lastPos, matchStart - lastPos));
|
||||||
|
|
||||||
|
if (!key.isEmpty() && variables.contains(key)) {
|
||||||
|
result.append(variables.value(key));
|
||||||
|
} else {
|
||||||
|
result.append(match.captured(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPos = matchStart + matchLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPos < templateStr.size()) {
|
||||||
|
result.append(templateStr.mid(lastPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Extracts all placeholder keys from a template string.
|
||||||
|
*
|
||||||
|
* @param templateStr The template string to scan.
|
||||||
|
* @return A list of unique keys found in the template (trimmed).
|
||||||
|
*/
|
||||||
|
inline QStringList extractTemplateKeys(const QString& templateStr) {
|
||||||
|
static const QRegularExpression placeholderRegex(
|
||||||
|
QStringLiteral(R"(\{\{\s*([^{}]+?)\s*\}\})"));
|
||||||
|
|
||||||
|
QSet<QString> seen;
|
||||||
|
QStringList keys;
|
||||||
|
|
||||||
|
QRegularExpressionMatchIterator it = placeholderRegex.globalMatch(templateStr);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
const QRegularExpressionMatch match = it.next();
|
||||||
|
const QString key = match.captured(1).trimmed();
|
||||||
|
if (!key.isEmpty() && !seen.contains(key)) {
|
||||||
|
seen.insert(key);
|
||||||
|
keys.append(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // TEXTTEMPLATE_HPP
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include "wallpaperservice.hpp"
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "utils/logger.hpp"
|
||||||
|
#include "utils/texttemplate.hpp"
|
||||||
|
|
||||||
|
using namespace GeneralLogger;
|
||||||
|
|
||||||
|
WallpaperService::WallpaperService(
|
||||||
|
const Config::ActionConfigItems& actionConfig,
|
||||||
|
QObject* parent)
|
||||||
|
: QObject(parent), m_actionConfig(actionConfig) {
|
||||||
|
m_previewDebounceTimer = new QTimer(this);
|
||||||
|
m_previewDebounceTimer->setSingleShot(true);
|
||||||
|
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime);
|
||||||
|
connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
_doPreview(*m_pendingImageData);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_previewProcess = new QProcess(this);
|
||||||
|
connect(m_previewProcess,
|
||||||
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
|
this,
|
||||||
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
|
Q_UNUSED(exitCode);
|
||||||
|
Q_UNUSED(exitStatus);
|
||||||
|
emit previewCompleted();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_selectProcess = new QProcess(this);
|
||||||
|
connect(m_selectProcess,
|
||||||
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
|
this,
|
||||||
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
|
Q_UNUSED(exitCode);
|
||||||
|
Q_UNUSED(exitStatus);
|
||||||
|
emit selectCompleted();
|
||||||
|
});
|
||||||
|
|
||||||
|
m_restoreProcess = new QProcess(this);
|
||||||
|
connect(m_restoreProcess,
|
||||||
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
|
this,
|
||||||
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
|
Q_UNUSED(exitCode);
|
||||||
|
Q_UNUSED(exitStatus);
|
||||||
|
emit restoreCompleted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::preview(const ImageData& imageData) {
|
||||||
|
m_pendingImageData = &imageData;
|
||||||
|
m_previewDebounceTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::select(const ImageData& imageData) {
|
||||||
|
if (m_selectProcess->state() != QProcess::NotRunning) {
|
||||||
|
warn("Previous select command is still running. Ignoring new command.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_doSelect(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::restore() {
|
||||||
|
if (m_restoreProcess->state() != QProcess::NotRunning) {
|
||||||
|
warn("Previous restore command is still running. Ignoring new command.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_doRestore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::_doPreview(const ImageData& imageData) {
|
||||||
|
QString path = imageData.getFullPath();
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_actionConfig.printPreview) {
|
||||||
|
std::cout << path.toStdString() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QHash<QString, QString> variables{
|
||||||
|
{"path", path},
|
||||||
|
{"name", imageData.getFileName()},
|
||||||
|
};
|
||||||
|
auto command = renderTemplate(m_actionConfig.onPreview, variables);
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_previewProcess->state() != QProcess::NotRunning) {
|
||||||
|
m_previewProcess->kill();
|
||||||
|
m_previewProcess->waitForFinished();
|
||||||
|
}
|
||||||
|
m_previewProcess->start("sh", QStringList() << "-c" << command);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::_doSelect(const ImageData& imageData) {
|
||||||
|
QString path = imageData.getFullPath();
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_actionConfig.printSelected) {
|
||||||
|
std::cout << path.toStdString() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QHash<QString, QString> variables{
|
||||||
|
{"path", path},
|
||||||
|
{"name", imageData.getFileName()},
|
||||||
|
};
|
||||||
|
auto command = renderTemplate(m_actionConfig.onSelected, variables);
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_selectProcess->start("sh", QStringList() << "-c" << command);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallpaperService::_doRestore() {
|
||||||
|
if (m_actionConfig.onRestore.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString command = renderTemplate(m_actionConfig.onRestore, m_actionConfig.saveState);
|
||||||
|
if (command.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_restoreProcess->start("sh", QStringList() << "-c" << command);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#ifndef WALLREEL_WALLPAPERSERVICE_HPP
|
||||||
|
#define WALLREEL_WALLPAPERSERVICE_HPP
|
||||||
|
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "configmgr.hpp"
|
||||||
|
#include "imagedata.hpp"
|
||||||
|
|
||||||
|
class WallpaperService : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
WallpaperService(
|
||||||
|
const Config::ActionConfigItems& actionConfig,
|
||||||
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void preview(const ImageData& imageData); // execute after 500ms of inactivity
|
||||||
|
void select(const ImageData& imageData); // execute immediately, ignore if already running
|
||||||
|
void restore(); // execute immediately, ignore if already running
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void previewCompleted();
|
||||||
|
void selectCompleted();
|
||||||
|
void restoreCompleted();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void _doPreview(const ImageData& imageData);
|
||||||
|
void _doSelect(const ImageData& imageData);
|
||||||
|
void _doRestore();
|
||||||
|
|
||||||
|
const Config::ActionConfigItems& m_actionConfig;
|
||||||
|
QTimer* m_previewDebounceTimer;
|
||||||
|
const ImageData* m_pendingImageData;
|
||||||
|
QProcess* m_previewProcess;
|
||||||
|
QProcess* m_selectProcess;
|
||||||
|
QProcess* m_restoreProcess;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WALLREEL_WALLPAPERSERVICE_HPP
|
||||||
@@ -19,10 +19,22 @@ Item {
|
|||||||
} else if (e.key === Qt.Key_Escape)
|
} else if (e.key === Qt.Key_Escape)
|
||||||
Qt.quit();
|
Qt.quit();
|
||||||
else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)
|
else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)
|
||||||
ImageModel.confirm(carousel.currentIndex);
|
ImageModel.selectImage(carousel.currentIndex);
|
||||||
else
|
else
|
||||||
e.accepted = false;
|
e.accepted = false;
|
||||||
}
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
ImageModel.previewImage(carousel.currentIndex);
|
||||||
|
root.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onCurrentIndexChanged() {
|
||||||
|
ImageModel.previewImage(carousel.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
target: carousel
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -42,9 +54,9 @@ Item {
|
|||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
model: ImageModel
|
model: ImageModel
|
||||||
itemWidth: Config.imageWidth
|
itemWidth: Config.imageWidth
|
||||||
itemHeight: Config.imageWidth / Config.aspectRatio
|
itemHeight: Config.imageHeight
|
||||||
focusedItemWidth: Config.imageFocusWidth
|
focusedItemWidth: Config.imageWidth * Config.imageFocusScale
|
||||||
focusedItemHeight: Config.imageFocusWidth / Config.aspectRatio
|
focusedItemHeight: Config.imageHeight * Config.imageFocusScale
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|||||||
+41
-13
@@ -1,17 +1,21 @@
|
|||||||
#include <qtypes.h>
|
#include <qobject.h>
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQmlContext>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "Core/configmgr.hpp"
|
#include "Core/configmgr.hpp"
|
||||||
#include "Core/imagemodel.hpp"
|
#include "Core/imagemodel.hpp"
|
||||||
#include "Core/imageprovider.hpp"
|
#include "Core/imageprovider.hpp"
|
||||||
|
#include "Core/palette/data.hpp"
|
||||||
|
#include "Core/palette/manager.hpp"
|
||||||
#include "Core/utils/logger.hpp"
|
#include "Core/utils/logger.hpp"
|
||||||
#include "Core/utils/misc.hpp"
|
#include "Core/utils/misc.hpp"
|
||||||
|
#include "Core/wallpaperservice.hpp"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -145,34 +149,58 @@ int main(int argc, char* argv[]) {
|
|||||||
return s_options.errorText.isEmpty() ? 0 : 1;
|
return s_options.errorText.isEmpty() ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Config config(
|
|
||||||
::getConfigDir(),
|
|
||||||
s_options.appendDirs,
|
|
||||||
s_options.configPath,
|
|
||||||
&a);
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
ImageProvider* imageProvider = new ImageProvider();
|
ImageProvider* imageProvider = new ImageProvider();
|
||||||
engine.addImageProvider(QLatin1String("processed"), imageProvider);
|
engine.addImageProvider(QLatin1String("processed"), imageProvider);
|
||||||
|
|
||||||
ImageModel imageModel(
|
auto config = new Config(
|
||||||
imageProvider,
|
::getConfigDir(),
|
||||||
config.getSortConfig(),
|
s_options.appendDirs,
|
||||||
config.getFocusImageSize(),
|
s_options.configPath,
|
||||||
|
imageProvider);
|
||||||
|
|
||||||
|
auto paletteMgr = new PaletteManager(
|
||||||
|
config->getPaletteConfig(),
|
||||||
&a);
|
&a);
|
||||||
|
engine.rootContext()->setContextProperty("PaletteManager", paletteMgr);
|
||||||
|
qRegisterMetaType<PaletteItem>();
|
||||||
|
qRegisterMetaType<ColorItem>();
|
||||||
|
|
||||||
|
auto imageModel = new ImageModel(
|
||||||
|
*imageProvider,
|
||||||
|
config->getSortConfig(),
|
||||||
|
config->getFocusImageSize(),
|
||||||
|
config);
|
||||||
|
|
||||||
|
auto wallpaperService = new WallpaperService(
|
||||||
|
config->getActionConfig(),
|
||||||
|
config);
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
imageModel,
|
||||||
|
&ImageModel::imageSelected,
|
||||||
|
wallpaperService,
|
||||||
|
&WallpaperService::select);
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
imageModel,
|
||||||
|
&ImageModel::imagePreviewed,
|
||||||
|
wallpaperService,
|
||||||
|
&WallpaperService::preview);
|
||||||
|
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
COREMODULE_URI,
|
COREMODULE_URI,
|
||||||
MODULE_VERSION_MAJOR,
|
MODULE_VERSION_MAJOR,
|
||||||
MODULE_VERSION_MINOR,
|
MODULE_VERSION_MINOR,
|
||||||
"Config",
|
"Config",
|
||||||
&config);
|
config);
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
COREMODULE_URI,
|
COREMODULE_URI,
|
||||||
MODULE_VERSION_MAJOR,
|
MODULE_VERSION_MAJOR,
|
||||||
MODULE_VERSION_MINOR,
|
MODULE_VERSION_MINOR,
|
||||||
"ImageModel",
|
"ImageModel",
|
||||||
&imageModel);
|
imageModel);
|
||||||
|
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
&engine,
|
&engine,
|
||||||
@@ -182,7 +210,7 @@ int main(int argc, char* argv[]) {
|
|||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
engine.loadFromModule(UIMODULE_URI, "Main");
|
engine.loadFromModule(UIMODULE_URI, "Main");
|
||||||
|
|
||||||
imageModel.loadAndProcess(config.getWallpapers());
|
imageModel->loadAndProcess(config->getWallpapers());
|
||||||
|
|
||||||
return a.exec();
|
return a.exec();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user