🚧 wip: chekkupointo, far from complete
This commit is contained in:
+1
-6
@@ -25,7 +25,7 @@ endif()
|
|||||||
|
|
||||||
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
configure_file(WallReel/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent Test)
|
find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.5)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
|
|
||||||
@@ -37,11 +37,6 @@ option(ADDRESS_SANITIZER "Enable Address Sanitizer for debugging." OFF)
|
|||||||
add_subdirectory(WallReel/Core)
|
add_subdirectory(WallReel/Core)
|
||||||
add_subdirectory(WallReel/UI)
|
add_subdirectory(WallReel/UI)
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
|
||||||
enable_testing()
|
|
||||||
add_subdirectory(Tests)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(ADDRESS_SANITIZER)
|
if(ADDRESS_SANITIZER)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
project(Tests LANGUAGES CXX)
|
|
||||||
|
|
||||||
find_package(Qt6 REQUIRED COMPONENTS 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::Test
|
|
||||||
${CORELIB_NAME}
|
|
||||||
)
|
|
||||||
|
|
||||||
# target_link_libraries(tst_imagemodel PRIVATE
|
|
||||||
# Qt6::Test
|
|
||||||
# ${CORELIB_NAME}
|
|
||||||
# )
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
#include <QtGui/qcolor.h>
|
|
||||||
|
|
||||||
#include <QFile>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QSignalSpy>
|
|
||||||
#include <QTemporaryDir>
|
|
||||||
#include <QTest>
|
|
||||||
|
|
||||||
#include "Config/manager.hpp"
|
|
||||||
|
|
||||||
using namespace WallReel::Core;
|
|
||||||
|
|
||||||
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::Manager 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().saveStateConfig.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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager 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::Manager config(m_tempDir.path(), {}, m_configPath);
|
|
||||||
QCOMPARE(config.getSortConfig().type, Config::SortType::Date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QTEST_MAIN(TestConfigMgr)
|
|
||||||
#include "tst_configmgr.moc"
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
#include <QDate>
|
|
||||||
#include <QSignalSpy>
|
|
||||||
#include <QTemporaryDir>
|
|
||||||
#include <QtTest>
|
|
||||||
|
|
||||||
#include "Config/manager.hpp"
|
|
||||||
#include "Image/model.hpp"
|
|
||||||
#include "Image/provider.hpp"
|
|
||||||
|
|
||||||
using namespace WallReel::Core;
|
|
||||||
|
|
||||||
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(Image::Model* 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(Image::Model* model) {
|
|
||||||
if (!model->isLoading()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QSignalSpy spy(model, &Image::Model::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;
|
|
||||||
|
|
||||||
Image::Provider provider;
|
|
||||||
Image::Model 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), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::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), Image::Model::NameRole).toString(), "c.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestImageModel::testSortDate() {
|
|
||||||
Config::SortConfigItems sortConfig;
|
|
||||||
sortConfig.type = Config::SortType::Date;
|
|
||||||
sortConfig.reverse = false;
|
|
||||||
|
|
||||||
Image::Provider provider;
|
|
||||||
Image::Model 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), Image::Model::NameRole).toString(), "c.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
|
|
||||||
// Reverse (Newest first)
|
|
||||||
sortConfig.reverse = true;
|
|
||||||
model.sortUpdate();
|
|
||||||
|
|
||||||
QCOMPARE(model.data(model.index(0), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::NameRole).toString(), "c.gif");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestImageModel::testSortSize() {
|
|
||||||
Config::SortConfigItems sortConfig;
|
|
||||||
sortConfig.type = Config::SortType::Size;
|
|
||||||
sortConfig.reverse = false;
|
|
||||||
|
|
||||||
Image::Provider provider;
|
|
||||||
Image::Model 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), Image::Model::NameRole).toString(), "c.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
|
|
||||||
// Reverse
|
|
||||||
sortConfig.reverse = true;
|
|
||||||
model.sortUpdate();
|
|
||||||
|
|
||||||
QCOMPARE(model.data(model.index(0), Image::Model::NameRole).toString(), "b.gif");
|
|
||||||
QCOMPARE(model.data(model.index(1), Image::Model::NameRole).toString(), "a.gif");
|
|
||||||
QCOMPARE(model.data(model.index(2), Image::Model::NameRole).toString(), "c.gif");
|
|
||||||
}
|
|
||||||
|
|
||||||
QTEST_MAIN(TestImageModel)
|
|
||||||
#include "tst_imagemodel.moc"
|
|
||||||
@@ -4,7 +4,6 @@ qt_add_qml_module(${CORELIB_NAME}
|
|||||||
SOURCES
|
SOURCES
|
||||||
Image/data.hpp Image/data.cpp
|
Image/data.hpp Image/data.cpp
|
||||||
Image/model.hpp Image/model.cpp
|
Image/model.hpp Image/model.cpp
|
||||||
Image/provider.hpp Image/provider.cpp
|
|
||||||
Palette/data.hpp
|
Palette/data.hpp
|
||||||
Palette/manager.hpp Palette/manager.cpp
|
Palette/manager.hpp Palette/manager.cpp
|
||||||
Palette/domcolor.hpp Palette/domcolor.cpp
|
Palette/domcolor.hpp Palette/domcolor.cpp
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
// action.saveState array [] Useful for restore command
|
// action.saveState array [] Useful for restore command
|
||||||
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
|
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
|
||||||
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
|
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
|
||||||
// action.saveState[].cmd string "" Command that outputs(to stdout) the value to save when executed
|
// action.saveState[].command string "" Command that outputs(to stdout) the value to save when executed
|
||||||
// action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout
|
// action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout
|
||||||
// action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
|
// action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
|
||||||
// action.quitOnSelected boolean false Whether to quit the application after confirming a wallpaper
|
// action.quitOnSelected boolean false Whether to quit the application after confirming a wallpaper
|
||||||
@@ -91,8 +91,8 @@ struct ActionConfigItems {
|
|||||||
struct SaveStateItem {
|
struct SaveStateItem {
|
||||||
QString key;
|
QString key;
|
||||||
QString defaultVal;
|
QString defaultVal;
|
||||||
QString cmd;
|
QString command;
|
||||||
int timeout = 3000;
|
int timeout = 3000; // milliseconds, 0 or negative means no timeout
|
||||||
};
|
};
|
||||||
|
|
||||||
QList<SaveStateItem> saveStateConfig;
|
QList<SaveStateItem> saveStateConfig;
|
||||||
|
|||||||
@@ -14,17 +14,19 @@
|
|||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
|
|
||||||
WallReel::Core::Config::Manager::Manager(
|
WallReel::Core::Config::Manager::Manager(
|
||||||
const QString& configDir,
|
const QDir& configDir,
|
||||||
const QStringList& searchDirs,
|
const QStringList& searchDirs,
|
||||||
const QString& configPath,
|
const QString& configPath,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QObject(parent), m_configDir(configDir) {
|
: QObject(parent), m_configDir(configDir) {
|
||||||
|
// Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName)
|
||||||
if (configPath.isEmpty()) {
|
if (configPath.isEmpty()) {
|
||||||
Logger::info(QString("Configuration directory: %1").arg(configDir));
|
Logger::info(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
|
||||||
_loadConfig(configDir + QDir::separator() + s_DefaultConfigFileName);
|
_loadConfig(m_configDir.absolutePath() + QDir::separator() + s_DefaultConfigFileName);
|
||||||
} else {
|
} else {
|
||||||
_loadConfig(configPath);
|
_loadConfig(configPath);
|
||||||
}
|
}
|
||||||
|
// Append additional search directories to the config
|
||||||
if (!searchDirs.isEmpty()) {
|
if (!searchDirs.isEmpty()) {
|
||||||
Logger::info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
|
Logger::info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
|
||||||
for (const auto& dir : searchDirs) {
|
for (const auto& dir : searchDirs) {
|
||||||
@@ -73,7 +75,7 @@ void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& ro
|
|||||||
if (config.contains("paths") && config["paths"].isArray()) {
|
if (config.contains("paths") && config["paths"].isArray()) {
|
||||||
for (const auto& item : config["paths"].toArray()) {
|
for (const auto& item : config["paths"].toArray()) {
|
||||||
if (item.isString()) {
|
if (item.isString()) {
|
||||||
m_wallpaperConfig.paths.append(Utils::expandPath(item.toString()));
|
m_wallpaperConfig.paths.append(Utils::ensureAbsolutePath(Utils::expandPath(item.toString())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& ro
|
|||||||
QJsonObject obj = item.toObject();
|
QJsonObject obj = item.toObject();
|
||||||
if (obj.contains("path") && obj["path"].isString()) {
|
if (obj.contains("path") && obj["path"].isString()) {
|
||||||
WallpaperConfigItems::WallpaperDirConfigItem dirConfig;
|
WallpaperConfigItems::WallpaperDirConfigItem dirConfig;
|
||||||
dirConfig.path = Utils::expandPath(obj["path"].toString());
|
dirConfig.path = Utils::ensureAbsolutePath(Utils::expandPath(obj["path"].toString()));
|
||||||
if (obj.contains("recursive") && obj["recursive"].isBool()) {
|
if (obj.contains("recursive") && obj["recursive"].isBool()) {
|
||||||
dirConfig.recursive = obj["recursive"].toBool();
|
dirConfig.recursive = obj["recursive"].toBool();
|
||||||
} else {
|
} else {
|
||||||
@@ -193,8 +195,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
|||||||
if (obj.contains("default") && obj["default"].isString()) {
|
if (obj.contains("default") && obj["default"].isString()) {
|
||||||
sItem.defaultVal = obj["default"].toString();
|
sItem.defaultVal = obj["default"].toString();
|
||||||
}
|
}
|
||||||
if (obj.contains("cmd") && obj["cmd"].isString()) {
|
if (obj.contains("command") && obj["command"].isString()) {
|
||||||
sItem.cmd = obj["cmd"].toString();
|
sItem.command = obj["command"].toString();
|
||||||
}
|
}
|
||||||
if (obj.contains("timeout") && obj["timeout"].isDouble()) {
|
if (obj.contains("timeout") && obj["timeout"].isDouble()) {
|
||||||
sItem.timeout = obj["timeout"].toInt();
|
sItem.timeout = obj["timeout"].toInt();
|
||||||
@@ -310,6 +312,8 @@ void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) {
|
|||||||
void WallReel::Core::Config::Manager::_loadWallpapers() {
|
void WallReel::Core::Config::Manager::_loadWallpapers() {
|
||||||
m_wallpapers.clear();
|
m_wallpapers.clear();
|
||||||
|
|
||||||
|
// Add paths first using a set to avoid duplicates
|
||||||
|
|
||||||
QSet<QString> paths;
|
QSet<QString> paths;
|
||||||
|
|
||||||
Logger::debug(QString("Loading wallpapers from %1 specified paths...").arg(m_wallpaperConfig.paths.size()));
|
Logger::debug(QString("Loading wallpapers from %1 specified paths...").arg(m_wallpaperConfig.paths.size()));
|
||||||
@@ -342,6 +346,8 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exclude paths that match any of the exclude regexes
|
||||||
|
|
||||||
Logger::debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
|
Logger::debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
|
||||||
QStringList toRemove;
|
QStringList toRemove;
|
||||||
for (const auto& exclude : std::as_const(m_wallpaperConfig.excludes)) {
|
for (const auto& exclude : std::as_const(m_wallpaperConfig.excludes)) {
|
||||||
@@ -365,10 +371,15 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger::info(QString("Found %1 files").arg(paths.size()));
|
Logger::info(QString("Found %1 images").arg(paths.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Config::Manager::captureState() {
|
void WallReel::Core::Config::Manager::captureState() {
|
||||||
|
if (m_pendingCaptures > 0) {
|
||||||
|
Logger::warn("State capture already in progress, ignoring new capture request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_pendingCaptures = 0;
|
m_pendingCaptures = 0;
|
||||||
|
|
||||||
const auto& items = m_actionConfig.saveStateConfig;
|
const auto& items = m_actionConfig.saveStateConfig;
|
||||||
@@ -378,7 +389,7 @@ void WallReel::Core::Config::Manager::captureState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& item : items) {
|
for (const auto& item : items) {
|
||||||
if (!item.cmd.isEmpty()) {
|
if (!item.command.isEmpty()) {
|
||||||
m_pendingCaptures++;
|
m_pendingCaptures++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,10 +400,10 @@ void WallReel::Core::Config::Manager::captureState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& item : items) {
|
for (const auto& item : items) {
|
||||||
if (item.cmd.isEmpty()) continue;
|
if (item.command.isEmpty()) continue;
|
||||||
|
|
||||||
QProcess* process = new QProcess(this);
|
QProcess* process = new QProcess(this);
|
||||||
QTimer* timer = nullptr;
|
QTimer* timer = nullptr; // Remains nullptr if no timeout is set for this item
|
||||||
if (item.timeout > 0) {
|
if (item.timeout > 0) {
|
||||||
timer = new QTimer(this);
|
timer = new QTimer(this);
|
||||||
timer->setSingleShot(true);
|
timer->setSingleShot(true);
|
||||||
@@ -451,7 +462,7 @@ void WallReel::Core::Config::Manager::captureState() {
|
|||||||
if (timer) {
|
if (timer) {
|
||||||
timer->start();
|
timer->start();
|
||||||
}
|
}
|
||||||
process->start("sh", QStringList() << "-c" << item.cmd);
|
process->start("sh", QStringList() << "-c" << item.command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||||
#define WALLREEL_CONFIGMGR_HPP
|
#define WALLREEL_CONFIGMGR_HPP
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
#include "data.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
namespace WallReel::Core::Config {
|
namespace WallReel::Core::Config {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Config Manager (QML Singleton)
|
||||||
|
*
|
||||||
|
* @details Config Manager, which:
|
||||||
|
* - Loads and parses the configuration file
|
||||||
|
* - Provides access to configuration values via getters and Q_PROPERTY
|
||||||
|
* - Scans for wallpapers based on the configuration
|
||||||
|
* - Captures state when requested and emits a signal when done
|
||||||
|
*/
|
||||||
class Manager : public QObject {
|
class Manager : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -15,17 +26,32 @@ class Manager : public QObject {
|
|||||||
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
|
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Manager object
|
||||||
|
*
|
||||||
|
* @param configDir The directory where the configuration file is located (should be the default location, i.e. $XDG_CONFIG_HOME/$appName)
|
||||||
|
* @param searchDirs Additional directories to search for wallpapers (not recursive)
|
||||||
|
* @param configPath Optional path to a specific configuration file (overrides the default config path)
|
||||||
|
* @param parent QObject parent
|
||||||
|
*
|
||||||
|
* @note The constructor will load the configuration and scan for wallpapers immediately.
|
||||||
|
*/
|
||||||
Manager(
|
Manager(
|
||||||
const QString& configDir,
|
const QDir& configDir,
|
||||||
const QStringList& searchDirs = {},
|
const QStringList& searchDirs = {},
|
||||||
const QString& configPath = "", // Override the default config path
|
const QString& configPath = "",
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
~Manager();
|
~Manager();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Getter, Get the list of wallpapers found
|
||||||
|
*
|
||||||
|
* @return const QStringList&
|
||||||
|
*/
|
||||||
const QStringList& getWallpapers() const { return m_wallpapers; }
|
const QStringList& getWallpapers() const { return m_wallpapers; }
|
||||||
|
|
||||||
qint64 getWallpaperCount() const { return m_wallpapers.size(); }
|
// Separate getters for each field in the configuration
|
||||||
|
|
||||||
const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
|
const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
|
||||||
|
|
||||||
@@ -37,6 +63,8 @@ class Manager : public QObject {
|
|||||||
|
|
||||||
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
||||||
|
|
||||||
|
// Getters for Q_PROPERTY
|
||||||
|
|
||||||
int getImageWidth() const { return m_styleConfig.imageWidth; }
|
int getImageWidth() const { return m_styleConfig.imageWidth; }
|
||||||
|
|
||||||
int getImageHeight() const { return m_styleConfig.imageHeight; }
|
int getImageHeight() const { return m_styleConfig.imageHeight; }
|
||||||
@@ -47,27 +75,38 @@ class Manager : public QObject {
|
|||||||
|
|
||||||
int getWindowHeight() const { return m_styleConfig.windowHeight; }
|
int getWindowHeight() const { return m_styleConfig.windowHeight; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A quick snippet to get the focused image size as a QSize
|
||||||
|
*
|
||||||
|
* @return QSize
|
||||||
|
*/
|
||||||
QSize getFocusImageSize() const {
|
QSize getFocusImageSize() const {
|
||||||
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Capture the current state of the configuration and emit stateCaptured() when done
|
||||||
|
*/
|
||||||
Q_INVOKABLE void captureState();
|
Q_INVOKABLE void captureState();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stateCaptured();
|
void stateCaptured();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Parse config
|
||||||
void _loadConfig(const QString& configPath);
|
void _loadConfig(const QString& configPath);
|
||||||
void _loadWallpapers();
|
|
||||||
void _loadWallpaperConfig(const QJsonObject& config);
|
void _loadWallpaperConfig(const QJsonObject& config);
|
||||||
void _loadPaletteConfig(const QJsonObject& config);
|
void _loadPaletteConfig(const QJsonObject& config);
|
||||||
void _loadActionConfig(const QJsonObject& config);
|
void _loadActionConfig(const QJsonObject& config);
|
||||||
void _loadStyleConfig(const QJsonObject& config);
|
void _loadStyleConfig(const QJsonObject& config);
|
||||||
void _loadSortConfig(const QJsonObject& config);
|
void _loadSortConfig(const QJsonObject& config);
|
||||||
|
// Load wallpapers
|
||||||
|
void _loadWallpapers();
|
||||||
|
// Callback for state capture results
|
||||||
void _onCaptureResult(const QString& key, const QString& value);
|
void _onCaptureResult(const QString& key, const QString& value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const QString m_configDir;
|
const QDir m_configDir;
|
||||||
WallpaperConfigItems m_wallpaperConfig;
|
WallpaperConfigItems m_wallpaperConfig;
|
||||||
PaletteConfigItems m_paletteConfig;
|
PaletteConfigItems m_paletteConfig;
|
||||||
ActionConfigItems m_actionConfig;
|
ActionConfigItems m_actionConfig;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
#include "data.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
|
|
||||||
#include "Palette/domcolor.hpp"
|
#include "Palette/domcolor.hpp"
|
||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
|
|
||||||
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString& path, const QSize& size) {
|
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(
|
||||||
Data* ret = new Data(path, size);
|
const QString& path,
|
||||||
|
const QSize& size,
|
||||||
|
const QDir& cacheDir) {
|
||||||
|
Data* ret = new Data(path, size, cacheDir);
|
||||||
if (!ret->isValid()) {
|
if (!ret->isValid()) {
|
||||||
delete ret;
|
delete ret;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -14,12 +18,32 @@ WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString&
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize)
|
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize, const QDir& cacheDir)
|
||||||
: m_file(path) {
|
: m_file(path), m_targetSize(targetSize) {
|
||||||
QImageReader reader(path);
|
m_id = _generateId(path, targetSize);
|
||||||
|
const auto cachePath = cacheDir.absoluteFilePath(_generateCacheFileName(m_id));
|
||||||
|
m_cachedFile = QFileInfo(cachePath);
|
||||||
|
|
||||||
|
// If cached file exists, use it directly
|
||||||
|
if (m_cachedFile.exists()) {
|
||||||
|
Logger::debug(QString("Cache hit for image: %1").arg(m_file.absoluteFilePath()));
|
||||||
|
if (!_loadFromCache()) {
|
||||||
|
Logger::warn(QString("Failed to load cached image from path: %1").arg(m_cachedFile.absoluteFilePath()));
|
||||||
|
if (!_loadFresh()) {
|
||||||
|
Logger::warn(QString("Failed to load fresh image from path: %1").arg(m_file.absoluteFilePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!_loadFresh()) {
|
||||||
|
Logger::warn(QString("Failed to load fresh image from path: %1").arg(m_file.absoluteFilePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallReel::Core::Image::Data::_loadFresh() {
|
||||||
|
QImageReader reader(m_file.absoluteFilePath());
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
Logger::warn(QString("Failed to load image from path: %1").arg(path));
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QSize originalSize = reader.size();
|
const QSize originalSize = reader.size();
|
||||||
@@ -27,40 +51,89 @@ WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize)
|
|||||||
// Scale the image to fit the target size while maintaining aspect ratio
|
// Scale the image to fit the target size while maintaining aspect ratio
|
||||||
QSize processSize = originalSize;
|
QSize processSize = originalSize;
|
||||||
if (originalSize.isValid()) {
|
if (originalSize.isValid()) {
|
||||||
double widthRatio = (double)targetSize.width() / originalSize.width();
|
double widthRatio = (double)m_targetSize.width() / originalSize.width();
|
||||||
double heightRatio = (double)targetSize.height() / originalSize.height();
|
double heightRatio = (double)m_targetSize.height() / originalSize.height();
|
||||||
double scaleFactor = std::max(widthRatio, heightRatio);
|
double scaleFactor = std::max(widthRatio, heightRatio);
|
||||||
processSize = originalSize * scaleFactor;
|
processSize = originalSize * scaleFactor;
|
||||||
|
|
||||||
|
// Use reader's built-in scaling if supported
|
||||||
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
|
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
|
||||||
reader.setScaledSize(processSize);
|
reader.setScaledSize(processSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reader.read(&m_image)) {
|
QImage image;
|
||||||
Logger::warn(QString("Failed to load image from path: %1").arg(path));
|
if (!reader.read(&image)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_image.width() > processSize.width() || m_image.height() > processSize.height()) {
|
// If reader doesn't support built-in scaling or the image still do not match the target size, do manual scaling
|
||||||
m_image = m_image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
if (image.size() != processSize) {
|
||||||
|
image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crop to target size if necessary
|
// Crop to target size if necessary
|
||||||
if (m_image.size() != targetSize) {
|
if (image.size() != m_targetSize) {
|
||||||
int x = (m_image.width() - targetSize.width()) / 2;
|
int x = (image.width() - m_targetSize.width()) / 2;
|
||||||
int y = (m_image.height() - targetSize.height()) / 2;
|
int y = (image.height() - m_targetSize.height()) / 2;
|
||||||
m_image = m_image.copy(x, y, targetSize.width(), targetSize.height());
|
image = image.copy(x, y, m_targetSize.width(), m_targetSize.height());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to GPU-friendly format
|
// Convert to GPU-friendly format
|
||||||
if (m_image.format() != QImage::Format_ARGB32_Premultiplied) {
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
m_image = m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ID
|
// Get dominant color
|
||||||
m_id = QString::number(qHash(m_file.absoluteFilePath()));
|
m_dominantColor = Palette::getDominantColor(image);
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
if (!image.save(m_cachedFile.absoluteFilePath())) {
|
||||||
|
Logger::warn(QString("Failed to save cached image to path: %1").arg(m_cachedFile.absoluteFilePath()));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
Logger::debug(QString("Cached image saved to path: %1").arg(m_cachedFile.absoluteFilePath()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WallReel::Core::Image::Data::_loadFromCache() {
|
||||||
|
QImageReader reader(m_cachedFile.absoluteFilePath());
|
||||||
|
if (!reader.canRead()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image;
|
||||||
|
if (!reader.read(&image)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Get dominant color
|
// Get dominant color
|
||||||
m_dominantColor = Palette::getDominantColor(m_image);
|
m_dominantColor = Palette::getDominantColor(image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage WallReel::Core::Image::Data::loadImage() const {
|
||||||
|
QImageReader reader(m_cachedFile.absoluteFilePath());
|
||||||
|
if (!reader.canRead()) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image;
|
||||||
|
if (!reader.read(&image)) {
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallReel::Core::Image::Data::_generateId(const QString& path, const QSize& size) {
|
||||||
|
auto info = QFileInfo(path);
|
||||||
|
auto key = QString("%1|%2|%3|%4x%5").arg(info.absoluteFilePath()).arg(info.lastModified().toSecsSinceEpoch()).arg(info.size()).arg(size.width()).arg(size.height());
|
||||||
|
|
||||||
|
QByteArray hash = QCryptographicHash::hash(key.toUtf8(), QCryptographicHash::Md5);
|
||||||
|
return QString::fromLatin1(hash.toHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WallReel::Core::Image::Data::_generateCacheFileName(const QString& id) {
|
||||||
|
return id + ".png";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,86 @@
|
|||||||
#ifndef WALLREEL_IMAGEDATA_HPP
|
#ifndef WALLREEL_IMAGEDATA_HPP
|
||||||
#define WALLREEL_IMAGEDATA_HPP
|
#define WALLREEL_IMAGEDATA_HPP
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
// Development note
|
||||||
|
/*
|
||||||
|
Current implementation of image loading and caching:
|
||||||
|
1. Generate a unique ID for the image based on:
|
||||||
|
- File path
|
||||||
|
- Last modified timestamp
|
||||||
|
- Target size (width x height)
|
||||||
|
and use it as the cache key.
|
||||||
|
2. Check if a cached version of the image exists in the cache directory using the generated ID.
|
||||||
|
- If so, load the image from the cache and construct the Data object accordingly.
|
||||||
|
- If not:
|
||||||
|
a. Load the original image from disk.
|
||||||
|
b. Scale and crop it to the target size.
|
||||||
|
c. Save the processed image to the cache directory using the generated ID as the filename.
|
||||||
|
d. Construct the Data object with the new generated image.
|
||||||
|
|
||||||
|
Why this approach - Main purposes
|
||||||
|
- Fast decoding:
|
||||||
|
By resizing and caching the image at the loading stage, the frontend can directly load the image
|
||||||
|
at a smaller size and avoid the overhead of downsizing large (8K+ for example) images in memory,
|
||||||
|
which can lead to significant performance improvements and reduced memory usage on the frontend.
|
||||||
|
- Memory efficiency:
|
||||||
|
- Avoid keeping pixel data in memory for all images, and only load on demand by the frontend. Even
|
||||||
|
keeping the resized image in memory can be costly if there are many, and the overhead of loading
|
||||||
|
small images from disk is generally negligible and acceptable.
|
||||||
|
- Resizing during loading fundamentally eliminates the possibility of the frontend storing large
|
||||||
|
images in memory. (and not all image formats support `sourceSize` property in the right way)
|
||||||
|
|
||||||
|
Possible improvements:
|
||||||
|
- Cache other properties of the image (dominant color for example) to entirely avoid processing the
|
||||||
|
image in loading stage. A simple key-value store should be sufficient.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
namespace WallReel::Core::Image {
|
namespace WallReel::Core::Image {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A Model class representing an image file
|
||||||
|
*
|
||||||
|
*/
|
||||||
class Data {
|
class Data {
|
||||||
QString m_id;
|
QString m_id; ///< Unique identifier for the image
|
||||||
QFileInfo m_file;
|
QFileInfo m_file; ///< File information of the image
|
||||||
QImage m_image;
|
QFileInfo m_cachedFile; ///< Cached file information for the loaded image
|
||||||
QColor m_dominantColor;
|
QSize m_targetSize; ///< Target size for the loaded image
|
||||||
QHash<QString, QString> m_colorCache;
|
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
|
||||||
|
QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name
|
||||||
|
|
||||||
Data(const QString& path, const QSize& size);
|
Data(const QString& path, const QSize& size, const QDir& cacheDir);
|
||||||
|
|
||||||
|
bool _loadFromCache();
|
||||||
|
|
||||||
|
bool _loadFresh();
|
||||||
|
|
||||||
|
static QString _generateId(const QString& path, const QSize& size);
|
||||||
|
|
||||||
|
static QString _generateCacheFileName(const QString& id);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static Data* create(const QString& path, const QSize& size);
|
/**
|
||||||
|
* @brief Factory method to create a Data instance from a file path. Returns nullptr if loading fails.
|
||||||
|
*
|
||||||
|
* @param path File path of the image
|
||||||
|
* @param size Target size for loaded image, the image will be scaled and cropped to this size and stored in memory
|
||||||
|
* @return Data*
|
||||||
|
*/
|
||||||
|
static Data* create(const QString& path, const QSize& size, const QDir& cacheDir);
|
||||||
|
|
||||||
const QImage& getImage() const { return m_image; }
|
QSize getTargetSize() const { return m_targetSize; }
|
||||||
|
|
||||||
const QString& getId() const { return m_id; }
|
QString getId() const { return m_id; }
|
||||||
|
|
||||||
bool isValid() const { return !m_image.isNull(); }
|
QUrl getUrl() const { return QUrl::fromLocalFile(m_cachedFile.absoluteFilePath()); }
|
||||||
|
|
||||||
|
bool isValid() const { return m_cachedFile.exists(); }
|
||||||
|
|
||||||
QString getFullPath() const { return m_file.absoluteFilePath(); }
|
QString getFullPath() const { return m_file.absoluteFilePath(); }
|
||||||
|
|
||||||
@@ -34,6 +92,8 @@ class Data {
|
|||||||
|
|
||||||
const QFileInfo& getFileInfo() const { return m_file; }
|
const QFileInfo& getFileInfo() const { return m_file; }
|
||||||
|
|
||||||
|
QImage loadImage() const;
|
||||||
|
|
||||||
const QColor& getDominantColor() const { return m_dominantColor; }
|
const QColor& getDominantColor() const { return m_dominantColor; }
|
||||||
|
|
||||||
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
|
|
||||||
WallReel::Core::Image::Model::Model(
|
WallReel::Core::Image::Model::Model(
|
||||||
Provider& provider,
|
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
QSize thumbnailSize,
|
const QDir& cacheDir,
|
||||||
|
const QSize& thumbnailSize,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QAbstractListModel(parent),
|
: QAbstractListModel(parent),
|
||||||
m_provider(provider),
|
|
||||||
m_sortConfig(sortConfig),
|
m_sortConfig(sortConfig),
|
||||||
|
m_cacheDir(cacheDir),
|
||||||
m_thumbnailSize(thumbnailSize),
|
m_thumbnailSize(thumbnailSize),
|
||||||
m_currentSortType(sortConfig.type) {
|
m_currentSortType(sortConfig.type) {
|
||||||
connect(
|
connect(
|
||||||
@@ -67,6 +67,8 @@ QVariant WallReel::Core::Image::Model::data(const QModelIndex& index, int role)
|
|||||||
switch (role) {
|
switch (role) {
|
||||||
case IdRole:
|
case IdRole:
|
||||||
return item->getId();
|
return item->getId();
|
||||||
|
case UrlRole:
|
||||||
|
return item->getUrl();
|
||||||
case PathRole:
|
case PathRole:
|
||||||
return item->getFullPath();
|
return item->getFullPath();
|
||||||
case NameRole:
|
case NameRole:
|
||||||
@@ -156,6 +158,8 @@ QVariant WallReel::Core::Image::Model::dataAt(int index, const QString& roleName
|
|||||||
const auto& item = m_data[actualIndex];
|
const auto& item = m_data[actualIndex];
|
||||||
if (roleName == "imgId") {
|
if (roleName == "imgId") {
|
||||||
return item->getId();
|
return item->getId();
|
||||||
|
} else if (roleName == "imgUrl") {
|
||||||
|
return item->getUrl();
|
||||||
} else if (roleName == "imgPath") {
|
} else if (roleName == "imgPath") {
|
||||||
return item->getFullPath();
|
return item->getFullPath();
|
||||||
} else if (roleName == "imgName") {
|
} else if (roleName == "imgName") {
|
||||||
@@ -177,13 +181,16 @@ void WallReel::Core::Image::Model::loadAndProcess(const QStringList& paths) {
|
|||||||
|
|
||||||
m_processedCount = 0;
|
m_processedCount = 0;
|
||||||
m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
|
m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
|
||||||
|
// These are all small objects so capturing by value should be fine
|
||||||
const auto thumbnailSize = m_thumbnailSize;
|
const auto thumbnailSize = m_thumbnailSize;
|
||||||
const auto counterPtr = &m_processedCount;
|
const auto counterPtr = &m_processedCount;
|
||||||
QFuture<Data*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
|
const auto cacheDir = m_cacheDir;
|
||||||
auto data = Data::create(path, thumbnailSize);
|
QFuture<Data*> future =
|
||||||
counterPtr->fetch_add(1, std::memory_order_relaxed);
|
QtConcurrent::mapped(paths, [thumbnailSize, counterPtr, cacheDir](const QString& path) {
|
||||||
return data;
|
auto data = Data::create(path, thumbnailSize, cacheDir);
|
||||||
});
|
counterPtr->fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
m_watcher.setFuture(future);
|
m_watcher.setFuture(future);
|
||||||
emit totalCountChanged();
|
emit totalCountChanged();
|
||||||
}
|
}
|
||||||
@@ -224,7 +231,6 @@ int WallReel::Core::Image::Model::_convertProxyIndex(int proxyIndex) const {
|
|||||||
|
|
||||||
void WallReel::Core::Image::Model::_clearData() {
|
void WallReel::Core::Image::Model::_clearData() {
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
m_provider.clear();
|
|
||||||
qDeleteAll(m_data);
|
qDeleteAll(m_data);
|
||||||
m_data.clear();
|
m_data.clear();
|
||||||
for (auto& i : m_sortIndices) {
|
for (auto& i : m_sortIndices) {
|
||||||
@@ -323,7 +329,6 @@ void WallReel::Core::Image::Model::_onProcessingFinished() {
|
|||||||
for (auto& data : results) {
|
for (auto& data : results) {
|
||||||
if (data && data->isValid()) {
|
if (data && data->isValid()) {
|
||||||
m_data.append(data);
|
m_data.append(data);
|
||||||
m_provider.insert(data);
|
|
||||||
} else {
|
} else {
|
||||||
Logger::warn("Failed to load image: " + (data ? data->getFullPath() : "null"));
|
Logger::warn("Failed to load image: " + (data ? data->getFullPath() : "null"));
|
||||||
delete data;
|
delete data;
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
#ifndef WALLREEL_IMAGEMODEL_HPP
|
#ifndef WALLREEL_IMAGEMODEL_HPP
|
||||||
#define WALLREEL_IMAGEMODEL_HPP
|
#define WALLREEL_IMAGEMODEL_HPP
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QDir>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include "Config/data.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "provider.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
|
// Development note
|
||||||
|
/*
|
||||||
|
What "Proxy index" is:
|
||||||
|
|
||||||
|
There are currently three layers of indices in the Model:
|
||||||
|
1. Actual index: The index of the image in the original data list (m_data), the order is not
|
||||||
|
guaranteed and can be considered random.
|
||||||
|
2. Sorted index: The index of the image after sorting, which is stored in m_sortIndices based on
|
||||||
|
different sort types. m_sortIndices are precomputed and does not change unless
|
||||||
|
m_data changes. In practice, the choise of which mapping from m_sortIndices to use
|
||||||
|
is determined by m_currentSortType.
|
||||||
|
3. Filtered index: The final mapping from the index exposed to the QML view to the actual data index,
|
||||||
|
which is stored in m_filteredIndices. m_filteredIndices is updated each time when
|
||||||
|
the sort type / sort order / search text changes, and only informs the layout to
|
||||||
|
update when its content actually changes.
|
||||||
|
|
||||||
|
Therefore, when acquiring data, the "proxied" index must first be converted to the "actual" index
|
||||||
|
by looking up m_filteredIndices, and then the actual data can be accessed from m_data.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
namespace WallReel::Core::Image {
|
namespace WallReel::Core::Image {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief An unrefactored (view)model class that manages and provides the image list and properties of the focused image.
|
||||||
|
*
|
||||||
|
*/
|
||||||
class Model : public QAbstractListModel {
|
class Model : public QAbstractListModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
// Controls which of the main screen and the loading screen should be shown
|
||||||
|
// and triggers callbacks when loading finished
|
||||||
Q_PROPERTY(bool isLoading READ isLoading NOTIFY isLoadingChanged)
|
Q_PROPERTY(bool isLoading READ isLoading NOTIFY isLoadingChanged)
|
||||||
|
// Indicates the progress of loading, used to update the progress bar in the loading screen
|
||||||
|
// Not neccessarily updated on every image loaded, but should be updated frequently enough to make the progress bar smooth
|
||||||
Q_PROPERTY(int processedCount READ processedCount NOTIFY progressChanged)
|
Q_PROPERTY(int processedCount READ processedCount NOTIFY progressChanged)
|
||||||
|
// Total count of images to be loaded, used to calculate the progress percentage
|
||||||
Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged)
|
Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged)
|
||||||
|
// Sorting related properties
|
||||||
|
// How this works:
|
||||||
|
// 1. User interact with QML control components
|
||||||
|
// 2. QML calls the setter of the corresponding property in the Model
|
||||||
|
// 3. Model changes its internal state and update the sort indices accordingly
|
||||||
|
// 4. Model emits signal and possibly update state on QML side (for stateless controls)
|
||||||
|
// 5. ... Continue on further updates (search filter / focused image properties / etc)
|
||||||
Q_PROPERTY(QString currentSortType READ currentSortType WRITE setCurrentSortType NOTIFY currentSortTypeChanged)
|
Q_PROPERTY(QString currentSortType READ currentSortType WRITE setCurrentSortType NOTIFY currentSortTypeChanged)
|
||||||
Q_PROPERTY(bool currentSortReverse READ currentSortReverse WRITE setCurrentSortReverse NOTIFY currentSortReverseChanged)
|
Q_PROPERTY(bool currentSortReverse READ currentSortReverse WRITE setCurrentSortReverse NOTIFY currentSortReverseChanged)
|
||||||
|
// Focused image related properties, updated when focused image changed
|
||||||
Q_PROPERTY(QString focusedName READ focusedName NOTIFY focusedNameChanged)
|
Q_PROPERTY(QString focusedName READ focusedName NOTIFY focusedNameChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -28,6 +65,7 @@ class Model : public QAbstractListModel {
|
|||||||
|
|
||||||
enum Roles {
|
enum Roles {
|
||||||
IdRole = Qt::UserRole + 1,
|
IdRole = Qt::UserRole + 1,
|
||||||
|
UrlRole,
|
||||||
PathRole,
|
PathRole,
|
||||||
NameRole
|
NameRole
|
||||||
};
|
};
|
||||||
@@ -35,6 +73,7 @@ class Model : public QAbstractListModel {
|
|||||||
QHash<int, QByteArray> roleNames() const override {
|
QHash<int, QByteArray> roleNames() const override {
|
||||||
return {
|
return {
|
||||||
{IdRole, "imgId"},
|
{IdRole, "imgId"},
|
||||||
|
{UrlRole, "imgUrl"}, // file:///...
|
||||||
{PathRole, "imgPath"},
|
{PathRole, "imgPath"},
|
||||||
{NameRole, "imgName"},
|
{NameRole, "imgName"},
|
||||||
};
|
};
|
||||||
@@ -43,9 +82,9 @@ class Model : public QAbstractListModel {
|
|||||||
// Constructor / Destructor
|
// Constructor / Destructor
|
||||||
|
|
||||||
Model(
|
Model(
|
||||||
Provider& provider,
|
|
||||||
const Config::SortConfigItems& sortConfig,
|
const Config::SortConfigItems& sortConfig,
|
||||||
QSize thumbnailSize,
|
const QDir& cacheDir,
|
||||||
|
const QSize& thumbnailSize,
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
~Model();
|
~Model();
|
||||||
@@ -93,18 +132,25 @@ class Model : public QAbstractListModel {
|
|||||||
private:
|
private:
|
||||||
int _convertProxyIndex(int proxyIndex) const;
|
int _convertProxyIndex(int proxyIndex) const;
|
||||||
void _clearData();
|
void _clearData();
|
||||||
|
// Update the corresponding mapping in m_sortIndices based on the current m_data and the given sort type
|
||||||
void _updateSortIndices(Config::SortType type);
|
void _updateSortIndices(Config::SortType type);
|
||||||
|
// Reobtain the properties of the focused image and emit corresponding signals
|
||||||
void _updateFocusedProperties();
|
void _updateFocusedProperties();
|
||||||
|
// Update m_filteredIndices, only calls layoutAboutToBeChanged and layoutChanged when the filtered result
|
||||||
|
// actually changes and informView is true
|
||||||
void _applySearchFilter(bool informView = true);
|
void _applySearchFilter(bool informView = true);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
// Properties
|
||||||
void isLoadingChanged();
|
void isLoadingChanged();
|
||||||
void progressChanged();
|
void progressChanged();
|
||||||
void totalCountChanged();
|
void totalCountChanged();
|
||||||
void currentSortTypeChanged();
|
void currentSortTypeChanged(); // -> _onSortMethodChanged
|
||||||
void currentSortReverseChanged();
|
void currentSortReverseChanged(); // -> _onSortMethodChanged
|
||||||
void focusedNameChanged();
|
void focusedNameChanged();
|
||||||
void searchTextChanged();
|
// emitted after search text changed and the filter is applied
|
||||||
|
void searchTextChanged(); // -> _onSearchTextChanged
|
||||||
|
// emiited when the focued image (is believed to be) changed
|
||||||
void focusedImageChanged();
|
void focusedImageChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
@@ -114,8 +160,8 @@ class Model : public QAbstractListModel {
|
|||||||
void _onSearchTextChanged();
|
void _onSearchTextChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Provider& m_provider;
|
|
||||||
const Config::SortConfigItems& m_sortConfig;
|
const Config::SortConfigItems& m_sortConfig;
|
||||||
|
QDir m_cacheDir;
|
||||||
QSize m_thumbnailSize;
|
QSize m_thumbnailSize;
|
||||||
|
|
||||||
QList<Data*> m_data;
|
QList<Data*> m_data;
|
||||||
@@ -131,9 +177,6 @@ class Model : public QAbstractListModel {
|
|||||||
// QTimer m_searchDebounceTimer;
|
// QTimer m_searchDebounceTimer;
|
||||||
// static constexpr int s_SearchDebounceIntervalMs = 300;
|
// static constexpr int s_SearchDebounceIntervalMs = 300;
|
||||||
|
|
||||||
QColor m_focusedColor{};
|
|
||||||
QString m_focusedColorName{};
|
|
||||||
|
|
||||||
QFutureWatcher<Data*> m_watcher;
|
QFutureWatcher<Data*> m_watcher;
|
||||||
bool m_isLoading = false;
|
bool m_isLoading = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
#include "provider.hpp"
|
|
||||||
|
|
||||||
QImage WallReel::Core::Image::Provider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
if (!m_images.contains(id)) {
|
|
||||||
return QImage();
|
|
||||||
}
|
|
||||||
Data* data = m_images[id];
|
|
||||||
if (size) {
|
|
||||||
*size = data->getImage().size();
|
|
||||||
}
|
|
||||||
return data->getImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallReel::Core::Image::Provider::insert(Data* data) {
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
m_images.insert(data->getId(), data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void WallReel::Core::Image::Provider::clear() {
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
m_images.clear();
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#ifndef WALLREEL_IMAGEPROVIDER_HPP
|
|
||||||
#define WALLREEL_IMAGEPROVIDER_HPP
|
|
||||||
|
|
||||||
#include <QHash>
|
|
||||||
#include <QMutex>
|
|
||||||
#include <QQuickImageProvider>
|
|
||||||
|
|
||||||
#include "data.hpp"
|
|
||||||
|
|
||||||
namespace WallReel::Core::Image {
|
|
||||||
|
|
||||||
class Provider : public QQuickImageProvider {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
Provider() : QQuickImageProvider(QQuickImageProvider::Image) {}
|
|
||||||
|
|
||||||
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
|
|
||||||
|
|
||||||
void insert(Data* data);
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QMutex m_mutex;
|
|
||||||
QHash<QString, Data*> m_images;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace WallReel::Core::Image
|
|
||||||
|
|
||||||
#endif // WALLREEL_IMAGEPROVIDER_HPP
|
|
||||||
@@ -22,6 +22,10 @@ struct ColorItem {
|
|||||||
bool operator==(const ColorItem& other) const {
|
bool operator==(const ColorItem& other) const {
|
||||||
return name == other.name;
|
return name == other.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isValid() const {
|
||||||
|
return !name.isEmpty() && color.isValid();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PaletteItem {
|
struct PaletteItem {
|
||||||
@@ -31,6 +35,8 @@ struct PaletteItem {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
QString name;
|
QString name;
|
||||||
|
// We need to keep the order of colors, and the number of colors is usually limited,
|
||||||
|
// so a flat list and O(n) lookup should be fine here.
|
||||||
QList<ColorItem> colors;
|
QList<ColorItem> colors;
|
||||||
|
|
||||||
Q_INVOKABLE QColor getColor(const QString& colorName) const {
|
Q_INVOKABLE QColor getColor(const QString& colorName) const {
|
||||||
@@ -40,9 +46,20 @@ struct PaletteItem {
|
|||||||
return QColor();
|
return QColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ColorItem getColorItem(const QString& colorName) const {
|
||||||
|
for (const auto& entry : colors) {
|
||||||
|
if (entry.name == colorName) return entry;
|
||||||
|
}
|
||||||
|
return ColorItem();
|
||||||
|
}
|
||||||
|
|
||||||
bool operator==(const PaletteItem& other) const {
|
bool operator==(const PaletteItem& other) const {
|
||||||
return name == other.name;
|
return name == other.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isValid() const {
|
||||||
|
return !name.isEmpty() && !colors.isEmpty();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Palette
|
} // namespace WallReel::Core::Palette
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
|
|
||||||
namespace WallReel::Core::Palette {
|
namespace WallReel::Core::Palette {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the dominant color of the given image.
|
||||||
|
*
|
||||||
|
* @param image The input image
|
||||||
|
* @return QColor An empty QColor() case error occurs, otherwise the dominant color of the image
|
||||||
|
*/
|
||||||
QColor getDominantColor(const QImage& image);
|
QColor getDominantColor(const QImage& image);
|
||||||
|
|
||||||
} // namespace WallReel::Core::Palette
|
} // namespace WallReel::Core::Palette
|
||||||
|
|||||||
@@ -72,16 +72,12 @@ void WallReel::Core::Palette::Manager::updateColor() {
|
|||||||
if (!m_selectedColor.has_value()) {
|
if (!m_selectedColor.has_value()) {
|
||||||
auto cached = imageData->getCachedColor(m_selectedPalette->name);
|
auto cached = imageData->getCachedColor(m_selectedPalette->name);
|
||||||
if (cached.has_value()) {
|
if (cached.has_value()) {
|
||||||
auto it = std::find_if(m_selectedPalette->colors.begin(),
|
auto found = m_selectedPalette.value().getColorItem(cached.value());
|
||||||
m_selectedPalette->colors.end(),
|
if (found.isValid()) {
|
||||||
[&](const ColorItem& item) {
|
|
||||||
return item.name == cached.value();
|
|
||||||
});
|
|
||||||
if (it != m_selectedPalette->colors.end()) {
|
|
||||||
Logger::debug("Using cached color match for image " + imageData->getFileName() +
|
Logger::debug("Using cached color match for image " + imageData->getFileName() +
|
||||||
": " + it->name);
|
": " + found.name);
|
||||||
m_displayColor = it->color;
|
m_displayColor = found.color;
|
||||||
m_displayColorName = it->name;
|
m_displayColorName = found.name;
|
||||||
hasResult = true;
|
hasResult = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,6 +85,15 @@ void WallReel::Core::Palette::Manager::updateColor() {
|
|||||||
auto matched = bestMatch(
|
auto matched = bestMatch(
|
||||||
imageData->getDominantColor(),
|
imageData->getDominantColor(),
|
||||||
m_selectedPalette.value().colors);
|
m_selectedPalette.value().colors);
|
||||||
|
// Use dominant color if no valid match found (possibly empty palette)
|
||||||
|
if (!matched.isValid()) {
|
||||||
|
Logger::debug("No valid color match found for image " + imageData->getFileName() +
|
||||||
|
", using dominant color: " + imageData->getDominantColor().name());
|
||||||
|
m_displayColor = imageData->getDominantColor();
|
||||||
|
m_displayColorName = "";
|
||||||
|
hasResult = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
Logger::debug("Computed color match for image " + imageData->getFileName() + ": " +
|
Logger::debug("Computed color match for image " + imageData->getFileName() + ": " +
|
||||||
matched.name);
|
matched.name);
|
||||||
imageData->cacheColor(m_selectedPalette->name, matched.name);
|
imageData->cacheColor(m_selectedPalette->name, matched.name);
|
||||||
|
|||||||
@@ -20,17 +20,15 @@ class Manager : public QObject {
|
|||||||
Image::Model& imageModel,
|
Image::Model& imageModel,
|
||||||
QObject* parent = nullptr);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
const QList<PaletteItem>& availablePalettes() const {
|
// Properties
|
||||||
return m_palettes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QColor& color() const {
|
const QList<PaletteItem>& availablePalettes() const { return m_palettes; }
|
||||||
return m_displayColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString& colorName() const {
|
const QColor& color() const { return m_displayColor; }
|
||||||
return m_displayColorName;
|
|
||||||
}
|
const QString& colorName() const { return m_displayColorName; }
|
||||||
|
|
||||||
|
// Setters
|
||||||
|
|
||||||
Q_INVOKABLE void setSelectedPalette(const QVariant& paletteVar) {
|
Q_INVOKABLE void setSelectedPalette(const QVariant& paletteVar) {
|
||||||
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
||||||
@@ -50,14 +48,32 @@ class Manager : public QObject {
|
|||||||
updateColor();
|
updateColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the name of the currently selected palette
|
||||||
|
*
|
||||||
|
* @return QString The name of the currently selected palette, or an empty string if no palette is selected
|
||||||
|
*/
|
||||||
QString getSelectedPaletteName() const {
|
QString getSelectedPaletteName() const {
|
||||||
return m_selectedPalette ? m_selectedPalette->name : QString();
|
return m_selectedPalette ? m_selectedPalette->name : QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the name of the currently selected color
|
||||||
|
*
|
||||||
|
* @return QString The name of the currently selected color, or an empty string if the color does not have
|
||||||
|
* a pretty name
|
||||||
|
*/
|
||||||
QString getCurrentColorName() const {
|
QString getCurrentColorName() const {
|
||||||
return m_displayColorName;
|
return m_displayColorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the hex string of the currently selected color
|
||||||
|
*
|
||||||
|
* @return QString The hex string of the currently selected color, or an empty string if the color is invalid
|
||||||
|
*/
|
||||||
QString getCurrentColorHex() const {
|
QString getCurrentColorHex() const {
|
||||||
return m_displayColor.isValid()
|
return m_displayColor.isValid()
|
||||||
? m_displayColor.name()
|
? m_displayColor.name()
|
||||||
@@ -65,7 +81,7 @@ class Manager : public QObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void updateColor();
|
void updateColor(); // <- Image::Model::focusedImageChanged
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void colorChanged();
|
void colorChanged();
|
||||||
@@ -75,6 +91,7 @@ class Manager : public QObject {
|
|||||||
Image::Model& m_imageModel;
|
Image::Model& m_imageModel;
|
||||||
|
|
||||||
QList<PaletteItem> m_palettes;
|
QList<PaletteItem> m_palettes;
|
||||||
|
// Null means auto
|
||||||
std::optional<PaletteItem> m_selectedPalette = std::nullopt;
|
std::optional<PaletteItem> m_selectedPalette = std::nullopt;
|
||||||
std::optional<ColorItem> m_selectedColor = std::nullopt;
|
std::optional<ColorItem> m_selectedColor = std::nullopt;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
|
|
||||||
namespace WallReel::Core::Palette {
|
namespace WallReel::Core::Palette {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find the best matching color from the candidates for the given target color.
|
||||||
|
*
|
||||||
|
* @param target
|
||||||
|
* @param candidates
|
||||||
|
* @return const ColorItem& The best matching color item, or an empty ColorItem if no candidates are provided
|
||||||
|
*/
|
||||||
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates);
|
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates);
|
||||||
|
|
||||||
} // namespace WallReel::Core::Palette
|
} // namespace WallReel::Core::Palette
|
||||||
|
|||||||
@@ -3,6 +3,31 @@
|
|||||||
|
|
||||||
#include "data.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
|
// License of Catppuccin - MIT
|
||||||
|
/*
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Catppuccin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace WallReel::Core::Palette {
|
namespace WallReel::Core::Palette {
|
||||||
|
|
||||||
inline const QList<PaletteItem> preDefinedPalettes = {
|
inline const QList<PaletteItem> preDefinedPalettes = {
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVari
|
|||||||
{"path", imageData.getFullPath()},
|
{"path", imageData.getFullPath()},
|
||||||
{"name", imageData.getFileName()},
|
{"name", imageData.getFileName()},
|
||||||
{"size", QString::number(imageData.getSize())},
|
{"size", QString::number(imageData.getSize())},
|
||||||
{"width", QString::number(imageData.getImage().width())},
|
{"width", QString::number(imageData.getTargetSize().width())},
|
||||||
{"height", QString::number(imageData.getImage().height())},
|
{"height", QString::number(imageData.getTargetSize().height())},
|
||||||
{"palette", palette},
|
{"palette", palette},
|
||||||
{"color", color},
|
{"color", color},
|
||||||
{"colorHex", hex},
|
{"colorHex", hex},
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ inline QString expandPath(const QString& path) {
|
|||||||
return QDir::cleanPath(expandedPath);
|
return QDir::cleanPath(expandedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert the given path to an absolute path. If it's already absolute, return as is.
|
||||||
|
* If it's relative, make it absolute based on the current working directory.
|
||||||
|
*
|
||||||
|
* @param path Input path
|
||||||
|
* @return QString Absolute path
|
||||||
|
*
|
||||||
|
* @note No guarantee that the returned path actually exists or is valid.
|
||||||
|
* @note Symbolic links are not resolved.
|
||||||
|
* @note The returned path is cleaned using QDir::cleanPath()
|
||||||
|
*/
|
||||||
|
inline QString ensureAbsolutePath(const QString& path) {
|
||||||
|
if (QDir::isAbsolutePath(path)) {
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return QDir::cleanPath(QDir::current().filePath(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Split the file name from a given path.
|
* @brief Split the file name from a given path.
|
||||||
*
|
*
|
||||||
@@ -122,7 +141,12 @@ inline bool checkImageFile(const QString& filePath) {
|
|||||||
return formats.contains(ext.toUtf8());
|
return formats.contains(ext.toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
inline QString getConfigDir() {
|
/**
|
||||||
|
* @brief Get the configuration directory for the application, and create it if it doesn't exist.
|
||||||
|
*
|
||||||
|
* @return QDir The configuration directory, typically ~/.config/AppName
|
||||||
|
*/
|
||||||
|
inline QDir getConfigDir() {
|
||||||
// This will be ~/.config/AppName, where AppName is the name of executable target in CMakeLists.txt
|
// This will be ~/.config/AppName, where AppName is the name of executable target in CMakeLists.txt
|
||||||
auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||||
if (configDir.isEmpty()) {
|
if (configDir.isEmpty()) {
|
||||||
@@ -132,6 +156,21 @@ inline QString getConfigDir() {
|
|||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the cache directory for the application, and create it if it doesn't exist.
|
||||||
|
*
|
||||||
|
* @return QDir The cache directory, typically ~/.cache/AppName
|
||||||
|
*/
|
||||||
|
inline QDir getCacheDir() {
|
||||||
|
// This will be ~/.cache/AppName, where AppName is the name of executable target in CMakeLists.txt
|
||||||
|
auto cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||||
|
if (cacheDir.isEmpty()) {
|
||||||
|
cacheDir = QDir::homePath() + QDir::separator() + ".cache" + QDir::separator() + APP_NAME;
|
||||||
|
}
|
||||||
|
QDir().mkpath(cacheDir);
|
||||||
|
return QDir(cacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace WallReel::Core::Utils
|
} // namespace WallReel::Core::Utils
|
||||||
|
|
||||||
#endif // WALLREEL_MISC_HPP
|
#endif // WALLREEL_MISC_HPP
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ void AppOptions::printHelp() {
|
|||||||
doReturn = true;
|
doReturn = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -C --clear-cache
|
||||||
|
void AppOptions::clearCache() {
|
||||||
|
QDir cacheDir = Utils::getCacheDir();
|
||||||
|
if (cacheDir.exists()) {
|
||||||
|
if (cacheDir.removeRecursively()) {
|
||||||
|
Logger::info("Cache cleared successfully.");
|
||||||
|
} else {
|
||||||
|
Logger::warn("Failed to clear cache.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger::info("Cache directory does not exist, nothing to clear.");
|
||||||
|
}
|
||||||
|
doReturn = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Print error message and help
|
// Print error message and help
|
||||||
void AppOptions::printError() {
|
void AppOptions::printError() {
|
||||||
if (!errorText.isEmpty()) {
|
if (!errorText.isEmpty()) {
|
||||||
@@ -53,6 +68,9 @@ void AppOptions::parseArgs(QApplication& app) {
|
|||||||
QCommandLineOption verboseOption(QStringList() << "V" << "verbose", "Set log level to DEBUG (default is INFO)");
|
QCommandLineOption verboseOption(QStringList() << "V" << "verbose", "Set log level to DEBUG (default is INFO)");
|
||||||
parser.addOption(verboseOption);
|
parser.addOption(verboseOption);
|
||||||
|
|
||||||
|
QCommandLineOption clearCacheOption(QStringList() << "C" << "clear-cache", "Clear the image cache and exit");
|
||||||
|
parser.addOption(clearCacheOption);
|
||||||
|
|
||||||
QCommandLineOption quietOption(QStringList() << "q" << "quiet", "Suppress all log output");
|
QCommandLineOption quietOption(QStringList() << "q" << "quiet", "Suppress all log output");
|
||||||
parser.addOption(quietOption);
|
parser.addOption(quietOption);
|
||||||
|
|
||||||
@@ -71,13 +89,18 @@ void AppOptions::parseArgs(QApplication& app) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parser.isSet(helpOption)) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (parser.isSet(versionOption)) {
|
if (parser.isSet(versionOption)) {
|
||||||
printVersion();
|
printVersion();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parser.isSet(helpOption)) {
|
if (parser.isSet(clearCacheOption)) {
|
||||||
printHelp();
|
clearCache();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class AppOptions {
|
|||||||
// -h --help
|
// -h --help
|
||||||
void printHelp();
|
void printHelp();
|
||||||
|
|
||||||
|
// -C --clear-cache
|
||||||
|
void clearCache();
|
||||||
|
|
||||||
// Print error message and help
|
// Print error message and help
|
||||||
void printError();
|
void printError();
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Item {
|
|||||||
id: img
|
id: img
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: "image://processed/" + model.imgId
|
source: model.imgUrl
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
cache: true
|
cache: true
|
||||||
|
|||||||
+2
-6
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "Core/Config/manager.hpp"
|
#include "Core/Config/manager.hpp"
|
||||||
#include "Core/Image/model.hpp"
|
#include "Core/Image/model.hpp"
|
||||||
#include "Core/Image/provider.hpp"
|
|
||||||
#include "Core/Palette/data.hpp"
|
#include "Core/Palette/data.hpp"
|
||||||
#include "Core/Palette/manager.hpp"
|
#include "Core/Palette/manager.hpp"
|
||||||
#include "Core/Service/manager.hpp"
|
#include "Core/Service/manager.hpp"
|
||||||
@@ -33,14 +32,11 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
auto* imageProvider = new Image::Provider();
|
|
||||||
engine.addImageProvider(QLatin1String("processed"), imageProvider);
|
|
||||||
|
|
||||||
auto config = new Config::Manager(
|
auto config = new Config::Manager(
|
||||||
Utils::getConfigDir(),
|
Utils::getConfigDir(),
|
||||||
s_options.appendDirs,
|
s_options.appendDirs,
|
||||||
s_options.configPath,
|
s_options.configPath,
|
||||||
imageProvider);
|
&engine);
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
COREMODULE_URI,
|
COREMODULE_URI,
|
||||||
MODULE_VERSION_MAJOR,
|
MODULE_VERSION_MAJOR,
|
||||||
@@ -49,8 +45,8 @@ int main(int argc, char* argv[]) {
|
|||||||
config);
|
config);
|
||||||
|
|
||||||
auto imageModel = new Image::Model(
|
auto imageModel = new Image::Model(
|
||||||
*imageProvider,
|
|
||||||
config->getSortConfig(),
|
config->getSortConfig(),
|
||||||
|
Utils::getCacheDir(),
|
||||||
config->getFocusImageSize(),
|
config->getFocusImageSize(),
|
||||||
config);
|
config);
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
|
|||||||
Reference in New Issue
Block a user