🚧 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_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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
+245
-113
@@ -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<void(const QJsonValue&)> 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<ConfigMapping>
|
||||
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<int>(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<void(const QDir&)> 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());
|
||||
|
||||
+84
-34
@@ -1,37 +1,55 @@
|
||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||
#define WALLREEL_CONFIGMGR_HPP
|
||||
|
||||
#include <qregularexpression.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QRegularExpression>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// 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<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 {
|
||||
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 {
|
||||
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<int>(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;
|
||||
|
||||
+11
-11
@@ -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()));
|
||||
}
|
||||
|
||||
+11
-11
@@ -5,30 +5,30 @@
|
||||
#include <QImage>
|
||||
|
||||
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:
|
||||
};
|
||||
|
||||
@@ -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<ImageData*>::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<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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ImageData*> m_data;
|
||||
QVector<ImageData*> m_data;
|
||||
|
||||
QFutureWatcher<ImageData*> m_watcher;
|
||||
bool m_isLoading = false;
|
||||
|
||||
std::atomic<int> 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
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
+41
-13
@@ -1,17 +1,21 @@
|
||||
#include <qtypes.h>
|
||||
#include <qobject.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDir>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
#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<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(
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user