diff --git a/CMakeLists.txt b/CMakeLists.txt index 5207860..dff1e76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ set(MODULE_VERSION_MINOR 0) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 23) + if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() @@ -23,15 +25,28 @@ endif() 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_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/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} WallReel/main.cpp ) diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..fe77169 --- /dev/null +++ b/Tests/CMakeLists.txt @@ -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 +) diff --git a/Tests/tst_configmgr.cpp b/Tests/tst_configmgr.cpp new file mode 100644 index 0000000..91a674e --- /dev/null +++ b/Tests/tst_configmgr.cpp @@ -0,0 +1,396 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#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" diff --git a/Tests/tst_imagemodel.cpp b/Tests/tst_imagemodel.cpp new file mode 100644 index 0000000..60b1077 --- /dev/null +++ b/Tests/tst_imagemodel.cpp @@ -0,0 +1,218 @@ +#include +#include +#include +#include + +#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 -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(mediumGIF), sizeof(mediumGIF)); + f.close(); + } + + { + QFile f(m_pathB); + QVERIFY(f.open(QIODevice::WriteOnly)); + f.write(reinterpret_cast(largeGIF), sizeof(largeGIF)); + f.close(); + } + + { + QFile f(m_pathC); + QVERIFY(f.open(QIODevice::WriteOnly)); + f.write(reinterpret_cast(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" diff --git a/WallReel/Core/CMakeLists.txt b/WallReel/Core/CMakeLists.txt index f172e1c..64cb752 100644 --- a/WallReel/Core/CMakeLists.txt +++ b/WallReel/Core/CMakeLists.txt @@ -8,6 +8,9 @@ qt_add_qml_module(${CORELIB_NAME} configmgr.hpp configmgr.cpp imagemodel.hpp imagemodel.cpp imageprovider.hpp imageprovider.cpp + wallpaperservice.hpp wallpaperservice.cpp + palette/data.hpp + palette/manager.hpp palette/manager.cpp ) target_link_libraries(${CORELIB_NAME} PRIVATE diff --git a/WallReel/Core/configmgr.cpp b/WallReel/Core/configmgr.cpp index 182d716..dca62dd 100644 --- a/WallReel/Core/configmgr.cpp +++ b/WallReel/Core/configmgr.cpp @@ -28,7 +28,9 @@ Config::Config( } if (!searchDirs.isEmpty()) { 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 ..."); @@ -56,117 +58,227 @@ void Config::_loadConfig(const QString& configPath) { const auto jsonObj = jsonDoc.object(); - struct ConfigMapping { - QString path; - QString key; - std::function parser; - }; + _loadWallpaperConfig(jsonObj); + _loadPaletteConfig(jsonObj); + _loadActionConfig(jsonObj); + _loadStyleConfig(jsonObj); + _loadSortConfig(jsonObj); +} - static const auto parseJsonArray = [](const QJsonValue& val, QStringList& list) { - if (val.isArray()) { - for (const auto& item : val.toArray()) { - if (item.isString()) { - list.append(::expandPath(item.toString())); +void Config::_loadWallpaperConfig(const QJsonObject& root) { + if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) { + return; + } + const QJsonObject& config = root["wallpaper"].toObject(); + + if (config.contains("paths") && config["paths"].isArray()) { + for (const auto& item : config["paths"].toArray()) { + if (item.isString()) { + m_wallpaperConfig.paths.append(::expandPath(item.toString())); + } + } + } + + if (config.contains("dirs") && config["dirs"].isArray()) { + for (const auto& item : config["dirs"].toArray()) { + if (item.isObject()) { + QJsonObject obj = item.toObject(); + if (obj.contains("path") && obj["path"].isString()) { + WallpaperConfigItems::WallpaperDirConfigItem dirConfig; + dirConfig.path = ::expandPath(obj["path"].toString()); + if (obj.contains("recursive") && obj["recursive"].isBool()) { + dirConfig.recursive = obj["recursive"].toBool(); + } else { + dirConfig.recursive = false; + } + m_wallpaperConfig.dirs.append(dirConfig); } } } - }; + } - std::vector - mappings = { - {"wallpaper.paths", "paths", [this](const QJsonValue& val) { - parseJsonArray(val, m_wallpaperConfig.paths); - }}, - {"wallpaper.dirs", "dirs", [this](const QJsonValue& val) { - parseJsonArray(val, m_wallpaperConfig.dirs); - }}, - {"wallpaper.excludes", "excludes", [this](const QJsonValue& val) { - parseJsonArray(val, m_wallpaperConfig.excludes); - }}, - {"action.confirm", "confirm", [this](const QJsonValue& val) { - if (val.isString()) { - m_actionConfig.confirm = ::expandPath(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)); - } - }}, - {"style.image_width", "image_width", [this](const QJsonValue& val) { - if (val.isDouble() && val.toDouble() > 0) { - 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 (val.isDouble() && val.toDouble() > 0) { - m_styleConfig.imageFocusWidth = val.toInt(); - debug(QString("Image focus width: %1").arg(m_styleConfig.imageFocusWidth)); - } - }}, - {"style.window_width", "window_width", [this](const QJsonValue& val) { - if (val.isDouble() && val.toDouble() > 0) { - m_styleConfig.windowWidth = val.toInt(); - debug(QString("Window width: %1").arg(m_styleConfig.windowWidth)); - } - }}, - {"style.window_height", "window_height", [this](const QJsonValue& val) { - if (val.isDouble() && val.toDouble() > 0) { - m_styleConfig.windowHeight = val.toInt(); - debug(QString("Window height: %1").arg(m_styleConfig.windowHeight)); - } - }}, - {"sort.type", "type", [this](const QJsonValue& val) { - if (val.isString()) { - QString type = val.toString().toLower(); - if (type == "none") { - m_sortConfig.type = SortType::None; - } else if (type == "name") { - m_sortConfig.type = SortType::Name; - } else if (type == "date") { - m_sortConfig.type = SortType::Date; - } else if (type == "size") { - m_sortConfig.type = SortType::Size; - } else { - warn(QString("Unknown sort type: %1").arg(type)); - } - } - debug(QString("Sort type: %1").arg(static_cast(m_sortConfig.type))); - }}, - {"sort.reverse", "reverse", [this](const QJsonValue& val) { - if (val.isBool()) { - 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(); + 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 { - debug(QString("Path '%1' not found").arg(pathParts.mid(0, i + 1).join('.'))); - return; + m_wallpaperConfig.excludes.append(regex); } } + } + } +} - 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)); +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()) { + m_actionConfig.onRestore = val.toString(); + } + } + if (config.contains("onSelected")) { + 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) { + m_styleConfig.imageWidth = val.toInt(); + } + } + if (config.contains("image_height")) { + const auto& val = config["image_height"]; + if (val.isDouble() && val.toDouble() > 0) { + m_styleConfig.imageHeight = val.toInt(); + } + } + 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) { + m_styleConfig.windowWidth = val.toInt(); + } + } + if (config.contains("window_height")) { + const auto& val = config["window_height"]; + if (val.isDouble() && val.toDouble() > 0) { + m_styleConfig.windowHeight = val.toInt(); + } + } +} + +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()) { + QString type = val.toString().toLower(); + if (type == "none") { + m_sortConfig.type = SortType::None; + } else if (type == "name") { + m_sortConfig.type = SortType::Name; + } else if (type == "date") { + m_sortConfig.type = SortType::Date; + } else if (type == "size") { + m_sortConfig.type = SortType::Size; + } else { + warn(QString("Unknown sort type: %1").arg(type)); + } + } + } + if (config.contains("reverse")) { + const auto& val = config["reverse"]; + if (val.isBool()) { + m_sortConfig.reverse = val.toBool(); + } } } @@ -181,22 +293,42 @@ void Config::_loadWallpapers() { } debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size())); - for (const QString& dirPath : std::as_const(m_wallpaperConfig.dirs)) { - QDir dir(dirPath); - if (checkDir(dirPath)) { - QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot); - for (const QString& file : std::as_const(files)) { - QString filePath = dir.filePath(file); - paths.insert(expandPath(filePath)); - } + for (const auto& dirConfig : std::as_const(m_wallpaperConfig.dirs)) { + if (checkDir(dirConfig.path)) { + std::function scanDir; + scanDir = [&](const QDir& d) { + QStringList files = d.entryList(QDir::Files | QDir::NoDotAndDotDot); + for (const QString& file : std::as_const(files)) { + QString filePath = d.filePath(file); + 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 { - 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())); - for (const QString& exclude : std::as_const(m_wallpaperConfig.excludes)) { - paths.remove(exclude); + QStringList toRemove; + 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()); diff --git a/WallReel/Core/configmgr.hpp b/WallReel/Core/configmgr.hpp index cc7fe35..ce5bc6d 100644 --- a/WallReel/Core/configmgr.hpp +++ b/WallReel/Core/configmgr.hpp @@ -1,37 +1,55 @@ #ifndef WALLREEL_CONFIGMGR_HPP #define WALLREEL_CONFIGMGR_HPP +#include + +#include #include -#include +#include #include #include #include // Config entries: // -// wallpaper.paths array image paths -// wallpaper.dirs array directories to search for images. -// all images in these directories will be added. -// NOT recursive. -// wallpaper.excludes array exclude patterns +// 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) // -// 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 -// style.image_width number width of each image -// style.image_focus_width number width of focused image -// style.window_width number fixed window width -// style.window_height number fixed window height +// 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) // -// sort.type string sorting type: "none", "name", "date", "size" -// sort.reverse boolean whether to reverse the sorting order +// 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 class Config : public QObject { Q_OBJECT - Q_PROPERTY(double aspectRatio READ getAspectRatio 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 windowHeight READ getWindowHeight CONSTANT) @@ -44,30 +62,56 @@ class Config : public QObject { }; struct WallpaperConfigItems { - QStringList paths; // "wallpaper.paths" - QStringList dirs; // "wallpaper.dirs" - QStringList excludes; // "wallpaper.excludes" + 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 { - QString confirm; // "action.confirm" + + QHash saveState; + QString onSelected; + QString onPreview; + QString onRestore; + int previewDebounceTime = 300; // milliseconds + bool printSelected = false; + bool printPreview = false; }; struct StyleConfigItems { - double aspectRatio = 1.6; // "style.aspect_ratio" - int imageWidth = 320; // "style.image_width" - int imageFocusWidth = 480; // "style.image_focus_width" - int windowWidth = 750; // "style.window_width" - int windowHeight = 500; // "style.window_height" + double imageFocusScale = 1.5; + int imageWidth = 320; + int imageHeight = 200; + int windowWidth = 750; + int windowHeight = 500; }; struct SortConfigItems { - SortType type = SortType::Name; // "sort.type" - bool reverse = false; // "sort.reverse" + SortType type = SortType::Name; + bool reverse = false; }; Config( - const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel" + const QString& configDir, const QStringList& searchDirs = {}, const QString& configPath = "", // Override the default config path QObject* parent = nullptr); @@ -80,26 +124,26 @@ class Config : public QObject { const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; } + const PaletteConfigItems& getPaletteConfig() const { return m_paletteConfig; } + const ActionConfigItems& getActionConfig() const { return m_actionConfig; } const StyleConfigItems& getStyleConfig() const { return m_styleConfig; } const SortConfigItems& getSortConfig() const { return m_sortConfig; } - double getAspectRatio() const { return m_styleConfig.aspectRatio; } - 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 getWindowHeight() const { return m_styleConfig.windowHeight; } QSize getFocusImageSize() const { - int width = m_styleConfig.imageFocusWidth; - int height = static_cast(width / m_styleConfig.aspectRatio); - return {width, height}; + return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale; } static const QString s_DefaultConfigFileName; @@ -108,9 +152,15 @@ class Config : public QObject { private: void _loadConfig(const QString& configPath); 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: WallpaperConfigItems m_wallpaperConfig; + PaletteConfigItems m_paletteConfig; ActionConfigItems m_actionConfig; StyleConfigItems m_styleConfig; SortConfigItems m_sortConfig; diff --git a/WallReel/Core/imagedata.cpp b/WallReel/Core/imagedata.cpp index e0712fa..9d1ded5 100644 --- a/WallReel/Core/imagedata.cpp +++ b/WallReel/Core/imagedata.cpp @@ -16,7 +16,7 @@ ImageData* ImageData::create(const QString& path, const QSize& size) { } ImageData::ImageData(const QString& path, const QSize& targetSize) - : file(path) { + : m_file(path) { QImageReader reader(path); if (!reader.canRead()) { 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)); return; } - if (image.width() > processSize.width() || image.height() > processSize.height()) { - image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + if (m_image.width() > processSize.width() || m_image.height() > processSize.height()) { + m_image = m_image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } // Crop to target size if necessary - if (image.size() != targetSize) { - int x = (image.width() - targetSize.width()) / 2; - int y = (image.height() - targetSize.height()) / 2; - image = image.copy(x, y, targetSize.width(), targetSize.height()); + if (m_image.size() != targetSize) { + int x = (m_image.width() - targetSize.width()) / 2; + int y = (m_image.height() - targetSize.height()) / 2; + m_image = m_image.copy(x, y, targetSize.width(), targetSize.height()); } // Convert to GPU-friendly format - if (image.format() != QImage::Format_ARGB32_Premultiplied) { - image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + if (m_image.format() != QImage::Format_ARGB32_Premultiplied) { + m_image = m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied); } // Create ID - id = QString::number(qHash(file.absoluteFilePath())); + m_id = QString::number(qHash(m_file.absoluteFilePath())); } diff --git a/WallReel/Core/imagedata.hpp b/WallReel/Core/imagedata.hpp index 512a907..429a365 100644 --- a/WallReel/Core/imagedata.hpp +++ b/WallReel/Core/imagedata.hpp @@ -5,30 +5,30 @@ #include class ImageData { - QString id; - QFileInfo file; - QImage image; + QString m_id; + QFileInfo m_file; + QImage m_image; ImageData(const QString& path, const QSize& size); public: 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: }; diff --git a/WallReel/Core/imagemodel.cpp b/WallReel/Core/imagemodel.cpp index c6af735..eaeb854 100644 --- a/WallReel/Core/imagemodel.cpp +++ b/WallReel/Core/imagemodel.cpp @@ -7,7 +7,7 @@ #include "imagedata.hpp" ImageModel::ImageModel( - ImageProvider* provider, + ImageProvider& provider, const Config::SortConfigItems& sortConfig, QSize thumbnailSize, QObject* parent) @@ -19,7 +19,7 @@ ImageModel::ImageModel( &m_watcher, &QFutureWatcher::finished, this, - &ImageModel::onProcessingFinished); + &ImageModel::_onProcessingFinished); connect( &m_progressUpdateTimer, &QTimer::timeout, @@ -33,6 +33,7 @@ ImageModel::~ImageModel() { m_watcher.cancel(); m_watcher.waitForFinished(); qDeleteAll(m_data); + m_data.clear(); } int ImageModel::rowCount(const QModelIndex& parent) const { @@ -67,16 +68,10 @@ void ImageModel::loadAndProcess(const QStringList& paths) { m_isLoading = true; emit isLoadingChanged(); - beginResetModel(); - if (!m_data.isEmpty()) { - qDeleteAll(m_data); - } - m_data.clear(); - m_provider->clear(); - endResetModel(); + _clearData(); m_processedCount = 0; - m_progressUpdateTimer.start(s_progressUpdateInterval); + m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs); const auto thumbnailSize = m_thumbnailSize; const auto counterPtr = &m_processedCount; QFuture 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); emit progressChanged(); } -void ImageModel::onProcessingFinished() { +void ImageModel::_onProcessingFinished() { auto results = m_watcher.future().results(); for (auto& data : results) { if (data && data->isValid()) { @@ -116,7 +111,7 @@ void ImageModel::onProcessingFinished() { m_progressUpdateTimer.stop(); emit progressChanged(); // emit isLoadingChanged(); - QTimer::singleShot(s_isLoadingUpdateInterval, this, [this]() { + QTimer::singleShot(s_IsLoadingUpdateIntervalMs, this, [this]() { emit isLoadingChanged(); }); } @@ -128,27 +123,29 @@ void ImageModel::sortUpdate() { if (!a || !b) { return false; } - bool result = false; + if (a == b) { + return false; + } + + ImageData* first = reverse ? b : a; + ImageData* second = reverse ? a : b; + switch (type) { case Config::SortType::Name: - result = QString::compare(a->getFileName(), b->getFileName(), Qt::CaseInsensitive) < 0; - break; + return QString::compare(first->getFileName(), second->getFileName(), Qt::CaseInsensitive) < 0; case Config::SortType::Date: - result = a->getLastModified() < b->getLastModified(); - break; + return first->getLastModified() < second->getLastModified(); case Config::SortType::Size: - result = a->getSize() < b->getSize(); - break; + return first->getSize() < second->getSize(); default: - break; + return false; } - return reverse ? !result : result; }); beginResetModel(); - m_provider->clear(); + m_provider.clear(); for (const auto& item : m_data) { - m_provider->insert(item); + m_provider.insert(item); } endResetModel(); } @@ -169,3 +166,31 @@ QVariant ImageModel::dataAt(int index, const QString& roleName) const { 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); + } +} diff --git a/WallReel/Core/imagemodel.hpp b/WallReel/Core/imagemodel.hpp index 2aa09f3..7987a2d 100644 --- a/WallReel/Core/imagemodel.hpp +++ b/WallReel/Core/imagemodel.hpp @@ -32,7 +32,7 @@ class ImageModel : public QAbstractListModel { } ImageModel( - ImageProvider* provider, + ImageProvider& provider, const Config::SortConfigItems& sortConfig, QSize thumbnailSize, QObject* parent = nullptr); @@ -57,30 +57,38 @@ class ImageModel : public QAbstractListModel { 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: void isLoadingChanged(); void progressChanged(); void totalCountChanged(); - void imageSelected(const QString& path); + void imageSelected(const ImageData& imageData); + void imagePreviewed(const ImageData& imageData); private slots: - void onProgressValueChanged(int value); - void onProcessingFinished(); + void _onProgressValueChanged(int value); + void _onProcessingFinished(); private: - ImageProvider* m_provider; + ImageProvider& m_provider; const Config::SortConfigItems& m_sortConfig; QSize m_thumbnailSize; - QList m_data; + QVector m_data; QFutureWatcher m_watcher; bool m_isLoading = false; std::atomic m_processedCount{0}; QTimer m_progressUpdateTimer; - static constexpr int s_progressUpdateInterval = 30; - static constexpr int s_isLoadingUpdateInterval = 50; + static constexpr int s_ProgressUpdateIntervalMs = 30; + static constexpr int s_IsLoadingUpdateIntervalMs = 50; }; #endif // WALLREEL_IMAGEMODEL_HPP diff --git a/WallReel/Core/palette/data.hpp b/WallReel/Core/palette/data.hpp new file mode 100644 index 0000000..59e00e4 --- /dev/null +++ b/WallReel/Core/palette/data.hpp @@ -0,0 +1,43 @@ +#ifndef WALLREEL_PALETTE_DATA_HPP +#define WALLREEL_PALETTE_DATA_HPP + +#include +#include +#include +#include + +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 colors MEMBER colors CONSTANT) + + public: + QString name; + QList 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 diff --git a/WallReel/Core/palette/manager.cpp b/WallReel/Core/palette/manager.cpp new file mode 100644 index 0000000..7c780cc --- /dev/null +++ b/WallReel/Core/palette/manager.cpp @@ -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 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); + } + } +} diff --git a/WallReel/Core/palette/manager.hpp b/WallReel/Core/palette/manager.hpp new file mode 100644 index 0000000..9c0fa11 --- /dev/null +++ b/WallReel/Core/palette/manager.hpp @@ -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 availablePalettes READ availablePalettes CONSTANT) + + public: + PaletteManager(const Config::PaletteConfigItems& config, + QObject* parent = nullptr); + + const QList& 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 m_palettes; +}; + +#endif // WALLREEL_PALETTE_MANAGER_HPP diff --git a/WallReel/Core/palette/predefined.hpp b/WallReel/Core/palette/predefined.hpp new file mode 100644 index 0000000..d7589f4 --- /dev/null +++ b/WallReel/Core/palette/predefined.hpp @@ -0,0 +1,88 @@ +#ifndef WALLREEL_PALETTES_PREDEFINED_HPP +#define WALLREEL_PALETTES_PREDEFINED_HPP + +#include "data.hpp" + +inline const QList 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 diff --git a/WallReel/Core/utils/colorextractor.hpp b/WallReel/Core/utils/colorextractor.hpp new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/WallReel/Core/utils/colorextractor.hpp @@ -0,0 +1 @@ + diff --git a/WallReel/Core/utils/texttemplate.hpp b/WallReel/Core/utils/texttemplate.hpp new file mode 100644 index 0000000..d045516 --- /dev/null +++ b/WallReel/Core/utils/texttemplate.hpp @@ -0,0 +1,138 @@ +#ifndef TEXTTEMPLATE_HPP +#define TEXTTEMPLATE_HPP + +#include +#include +#include + +/** + * @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& 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& 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 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 diff --git a/WallReel/Core/wallpaperservice.cpp b/WallReel/Core/wallpaperservice.cpp new file mode 100644 index 0000000..206e962 --- /dev/null +++ b/WallReel/Core/wallpaperservice.cpp @@ -0,0 +1,133 @@ +#include "wallpaperservice.hpp" + +#include +#include + +#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::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::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::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 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 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); +} diff --git a/WallReel/Core/wallpaperservice.hpp b/WallReel/Core/wallpaperservice.hpp new file mode 100644 index 0000000..ca641d2 --- /dev/null +++ b/WallReel/Core/wallpaperservice.hpp @@ -0,0 +1,41 @@ +#ifndef WALLREEL_WALLPAPERSERVICE_HPP +#define WALLREEL_WALLPAPERSERVICE_HPP + +#include +#include + +#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 diff --git a/WallReel/UI/Pages/CarouselScreen.qml b/WallReel/UI/Pages/CarouselScreen.qml index 5a21217..85bb6ac 100644 --- a/WallReel/UI/Pages/CarouselScreen.qml +++ b/WallReel/UI/Pages/CarouselScreen.qml @@ -19,10 +19,22 @@ Item { } else if (e.key === Qt.Key_Escape) Qt.quit(); else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter) - ImageModel.confirm(carousel.currentIndex); + ImageModel.selectImage(carousel.currentIndex); else e.accepted = false; } + Component.onCompleted: { + ImageModel.previewImage(carousel.currentIndex); + root.forceActiveFocus(); + } + + Connections { + function onCurrentIndexChanged() { + ImageModel.previewImage(carousel.currentIndex); + } + + target: carousel + } ColumnLayout { anchors.fill: parent @@ -42,9 +54,9 @@ Item { Layout.fillHeight: true model: ImageModel itemWidth: Config.imageWidth - itemHeight: Config.imageWidth / Config.aspectRatio - focusedItemWidth: Config.imageFocusWidth - focusedItemHeight: Config.imageFocusWidth / Config.aspectRatio + itemHeight: Config.imageHeight + focusedItemWidth: Config.imageWidth * Config.imageFocusScale + focusedItemHeight: Config.imageHeight * Config.imageFocusScale MouseArea { anchors.fill: parent diff --git a/WallReel/main.cpp b/WallReel/main.cpp index 331a804..e97209b 100644 --- a/WallReel/main.cpp +++ b/WallReel/main.cpp @@ -1,17 +1,21 @@ -#include +#include #include #include #include #include +#include #include #include #include "Core/configmgr.hpp" #include "Core/imagemodel.hpp" #include "Core/imageprovider.hpp" +#include "Core/palette/data.hpp" +#include "Core/palette/manager.hpp" #include "Core/utils/logger.hpp" #include "Core/utils/misc.hpp" +#include "Core/wallpaperservice.hpp" #include "version.h" /** @@ -145,34 +149,58 @@ int main(int argc, char* argv[]) { return s_options.errorText.isEmpty() ? 0 : 1; } - Config config( - ::getConfigDir(), - s_options.appendDirs, - s_options.configPath, - &a); QQmlApplicationEngine engine; ImageProvider* imageProvider = new ImageProvider(); engine.addImageProvider(QLatin1String("processed"), imageProvider); - ImageModel imageModel( - imageProvider, - config.getSortConfig(), - config.getFocusImageSize(), + auto config = new Config( + ::getConfigDir(), + s_options.appendDirs, + s_options.configPath, + imageProvider); + + auto paletteMgr = new PaletteManager( + config->getPaletteConfig(), &a); + engine.rootContext()->setContextProperty("PaletteManager", paletteMgr); + qRegisterMetaType(); + qRegisterMetaType(); + + 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( COREMODULE_URI, MODULE_VERSION_MAJOR, MODULE_VERSION_MINOR, "Config", - &config); + config); qmlRegisterSingletonInstance( COREMODULE_URI, MODULE_VERSION_MAJOR, MODULE_VERSION_MINOR, "ImageModel", - &imageModel); + imageModel); QObject::connect( &engine, @@ -182,7 +210,7 @@ int main(int argc, char* argv[]) { Qt::QueuedConnection); engine.loadFromModule(UIMODULE_URI, "Main"); - imageModel.loadAndProcess(config.getWallpapers()); + imageModel->loadAndProcess(config->getWallpapers()); return a.exec(); }