🚧 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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -37,11 +37,6 @@ 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")
|
||||
|
||||
@@ -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
|
||||
Image/data.hpp Image/data.cpp
|
||||
Image/model.hpp Image/model.cpp
|
||||
Image/provider.hpp Image/provider.cpp
|
||||
Palette/data.hpp
|
||||
Palette/manager.hpp Palette/manager.cpp
|
||||
Palette/domcolor.hpp Palette/domcolor.cpp
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
// action.saveState array [] Useful for restore command
|
||||
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
|
||||
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
|
||||
// action.saveState[].cmd string "" Command that outputs(to stdout) the value to save when executed
|
||||
// action.saveState[].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.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
|
||||
@@ -91,8 +91,8 @@ struct ActionConfigItems {
|
||||
struct SaveStateItem {
|
||||
QString key;
|
||||
QString defaultVal;
|
||||
QString cmd;
|
||||
int timeout = 3000;
|
||||
QString command;
|
||||
int timeout = 3000; // milliseconds, 0 or negative means no timeout
|
||||
};
|
||||
|
||||
QList<SaveStateItem> saveStateConfig;
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
#include "logger.hpp"
|
||||
|
||||
WallReel::Core::Config::Manager::Manager(
|
||||
const QString& configDir,
|
||||
const QDir& configDir,
|
||||
const QStringList& searchDirs,
|
||||
const QString& configPath,
|
||||
QObject* parent)
|
||||
: QObject(parent), m_configDir(configDir) {
|
||||
// Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName)
|
||||
if (configPath.isEmpty()) {
|
||||
Logger::info(QString("Configuration directory: %1").arg(configDir));
|
||||
_loadConfig(configDir + QDir::separator() + s_DefaultConfigFileName);
|
||||
Logger::info(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
|
||||
_loadConfig(m_configDir.absolutePath() + QDir::separator() + s_DefaultConfigFileName);
|
||||
} else {
|
||||
_loadConfig(configPath);
|
||||
}
|
||||
// Append additional search directories to the config
|
||||
if (!searchDirs.isEmpty()) {
|
||||
Logger::info(QString("Additional search directories: %1").arg(searchDirs.join(", ")));
|
||||
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()) {
|
||||
for (const auto& item : config["paths"].toArray()) {
|
||||
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();
|
||||
if (obj.contains("path") && obj["path"].isString()) {
|
||||
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()) {
|
||||
dirConfig.recursive = obj["recursive"].toBool();
|
||||
} else {
|
||||
@@ -193,8 +195,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
||||
if (obj.contains("default") && obj["default"].isString()) {
|
||||
sItem.defaultVal = obj["default"].toString();
|
||||
}
|
||||
if (obj.contains("cmd") && obj["cmd"].isString()) {
|
||||
sItem.cmd = obj["cmd"].toString();
|
||||
if (obj.contains("command") && obj["command"].isString()) {
|
||||
sItem.command = obj["command"].toString();
|
||||
}
|
||||
if (obj.contains("timeout") && obj["timeout"].isDouble()) {
|
||||
sItem.timeout = obj["timeout"].toInt();
|
||||
@@ -310,6 +312,8 @@ void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) {
|
||||
void WallReel::Core::Config::Manager::_loadWallpapers() {
|
||||
m_wallpapers.clear();
|
||||
|
||||
// Add paths first using a set to avoid duplicates
|
||||
|
||||
QSet<QString> paths;
|
||||
|
||||
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()));
|
||||
QStringList toRemove;
|
||||
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() {
|
||||
if (m_pendingCaptures > 0) {
|
||||
Logger::warn("State capture already in progress, ignoring new capture request");
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingCaptures = 0;
|
||||
|
||||
const auto& items = m_actionConfig.saveStateConfig;
|
||||
@@ -378,7 +389,7 @@ void WallReel::Core::Config::Manager::captureState() {
|
||||
}
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (!item.cmd.isEmpty()) {
|
||||
if (!item.command.isEmpty()) {
|
||||
m_pendingCaptures++;
|
||||
}
|
||||
}
|
||||
@@ -389,10 +400,10 @@ void WallReel::Core::Config::Manager::captureState() {
|
||||
}
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (item.cmd.isEmpty()) continue;
|
||||
if (item.command.isEmpty()) continue;
|
||||
|
||||
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) {
|
||||
timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
@@ -451,7 +462,7 @@ void WallReel::Core::Config::Manager::captureState() {
|
||||
if (timer) {
|
||||
timer->start();
|
||||
}
|
||||
process->start("sh", QStringList() << "-c" << item.cmd);
|
||||
process->start("sh", QStringList() << "-c" << item.command);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||
#define WALLREEL_CONFIGMGR_HPP
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include "data.hpp"
|
||||
|
||||
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 {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -15,17 +26,32 @@ class Manager : public QObject {
|
||||
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
|
||||
|
||||
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(
|
||||
const QString& configDir,
|
||||
const QDir& configDir,
|
||||
const QStringList& searchDirs = {},
|
||||
const QString& configPath = "", // Override the default config path
|
||||
const QString& configPath = "",
|
||||
QObject* parent = nullptr);
|
||||
|
||||
~Manager();
|
||||
|
||||
/**
|
||||
* @brief Getter, Get the list of wallpapers found
|
||||
*
|
||||
* @return const QStringList&
|
||||
*/
|
||||
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; }
|
||||
|
||||
@@ -37,6 +63,8 @@ class Manager : public QObject {
|
||||
|
||||
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
||||
|
||||
// Getters for Q_PROPERTY
|
||||
|
||||
int getImageWidth() const { return m_styleConfig.imageWidth; }
|
||||
|
||||
int getImageHeight() const { return m_styleConfig.imageHeight; }
|
||||
@@ -47,27 +75,38 @@ class Manager : public QObject {
|
||||
|
||||
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 {
|
||||
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();
|
||||
|
||||
signals:
|
||||
void stateCaptured();
|
||||
|
||||
private:
|
||||
// Parse config
|
||||
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);
|
||||
// Load wallpapers
|
||||
void _loadWallpapers();
|
||||
// Callback for state capture results
|
||||
void _onCaptureResult(const QString& key, const QString& value);
|
||||
|
||||
private:
|
||||
const QString m_configDir;
|
||||
const QDir m_configDir;
|
||||
WallpaperConfigItems m_wallpaperConfig;
|
||||
PaletteConfigItems m_paletteConfig;
|
||||
ActionConfigItems m_actionConfig;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
#include "data.hpp"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QImageReader>
|
||||
|
||||
#include "Palette/domcolor.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString& path, const QSize& size) {
|
||||
Data* ret = new Data(path, size);
|
||||
WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(
|
||||
const QString& path,
|
||||
const QSize& size,
|
||||
const QDir& cacheDir) {
|
||||
Data* ret = new Data(path, size, cacheDir);
|
||||
if (!ret->isValid()) {
|
||||
delete ret;
|
||||
return nullptr;
|
||||
@@ -14,12 +18,32 @@ WallReel::Core::Image::Data* WallReel::Core::Image::Data::create(const QString&
|
||||
return ret;
|
||||
}
|
||||
|
||||
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize)
|
||||
: m_file(path) {
|
||||
QImageReader reader(path);
|
||||
WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize, const QDir& cacheDir)
|
||||
: m_file(path), m_targetSize(targetSize) {
|
||||
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()) {
|
||||
Logger::warn(QString("Failed to load image from path: %1").arg(path));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
QSize processSize = originalSize;
|
||||
if (originalSize.isValid()) {
|
||||
double widthRatio = (double)targetSize.width() / originalSize.width();
|
||||
double heightRatio = (double)targetSize.height() / originalSize.height();
|
||||
double widthRatio = (double)m_targetSize.width() / originalSize.width();
|
||||
double heightRatio = (double)m_targetSize.height() / originalSize.height();
|
||||
double scaleFactor = std::max(widthRatio, heightRatio);
|
||||
processSize = originalSize * scaleFactor;
|
||||
|
||||
// Use reader's built-in scaling if supported
|
||||
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
|
||||
reader.setScaledSize(processSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (!reader.read(&m_image)) {
|
||||
Logger::warn(QString("Failed to load image from path: %1").arg(path));
|
||||
return;
|
||||
QImage image;
|
||||
if (!reader.read(&image)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_image.width() > processSize.width() || m_image.height() > processSize.height()) {
|
||||
m_image = m_image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
// If reader doesn't support built-in scaling or the image still do not match the target size, do manual scaling
|
||||
if (image.size() != processSize) {
|
||||
image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
// Crop to target size if necessary
|
||||
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());
|
||||
if (image.size() != m_targetSize) {
|
||||
int x = (image.width() - m_targetSize.width()) / 2;
|
||||
int y = (image.height() - m_targetSize.height()) / 2;
|
||||
image = image.copy(x, y, m_targetSize.width(), m_targetSize.height());
|
||||
}
|
||||
|
||||
// Convert to GPU-friendly format
|
||||
if (m_image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
m_image = m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
|
||||
// Create ID
|
||||
m_id = QString::number(qHash(m_file.absoluteFilePath()));
|
||||
|
||||
// Get dominant color
|
||||
m_dominantColor = Palette::getDominantColor(m_image);
|
||||
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
|
||||
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
|
||||
#define WALLREEL_IMAGEDATA_HPP
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#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 {
|
||||
|
||||
/**
|
||||
* @brief A Model class representing an image file
|
||||
*
|
||||
*/
|
||||
class Data {
|
||||
QString m_id;
|
||||
QFileInfo m_file;
|
||||
QImage m_image;
|
||||
QColor m_dominantColor;
|
||||
QHash<QString, QString> m_colorCache;
|
||||
QString m_id; ///< Unique identifier for the image
|
||||
QFileInfo m_file; ///< File information of the image
|
||||
QFileInfo m_cachedFile; ///< Cached file information for the loaded image
|
||||
QSize m_targetSize; ///< Target size for the loaded image
|
||||
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:
|
||||
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(); }
|
||||
|
||||
@@ -34,6 +92,8 @@ class Data {
|
||||
|
||||
const QFileInfo& getFileInfo() const { return m_file; }
|
||||
|
||||
QImage loadImage() const;
|
||||
|
||||
const QColor& getDominantColor() const { return m_dominantColor; }
|
||||
|
||||
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
#include "logger.hpp"
|
||||
|
||||
WallReel::Core::Image::Model::Model(
|
||||
Provider& provider,
|
||||
const Config::SortConfigItems& sortConfig,
|
||||
QSize thumbnailSize,
|
||||
const QDir& cacheDir,
|
||||
const QSize& thumbnailSize,
|
||||
QObject* parent)
|
||||
: QAbstractListModel(parent),
|
||||
m_provider(provider),
|
||||
m_sortConfig(sortConfig),
|
||||
m_cacheDir(cacheDir),
|
||||
m_thumbnailSize(thumbnailSize),
|
||||
m_currentSortType(sortConfig.type) {
|
||||
connect(
|
||||
@@ -67,6 +67,8 @@ QVariant WallReel::Core::Image::Model::data(const QModelIndex& index, int role)
|
||||
switch (role) {
|
||||
case IdRole:
|
||||
return item->getId();
|
||||
case UrlRole:
|
||||
return item->getUrl();
|
||||
case PathRole:
|
||||
return item->getFullPath();
|
||||
case NameRole:
|
||||
@@ -156,6 +158,8 @@ QVariant WallReel::Core::Image::Model::dataAt(int index, const QString& roleName
|
||||
const auto& item = m_data[actualIndex];
|
||||
if (roleName == "imgId") {
|
||||
return item->getId();
|
||||
} else if (roleName == "imgUrl") {
|
||||
return item->getUrl();
|
||||
} else if (roleName == "imgPath") {
|
||||
return item->getFullPath();
|
||||
} else if (roleName == "imgName") {
|
||||
@@ -177,10 +181,13 @@ void WallReel::Core::Image::Model::loadAndProcess(const QStringList& paths) {
|
||||
|
||||
m_processedCount = 0;
|
||||
m_progressUpdateTimer.start(s_ProgressUpdateIntervalMs);
|
||||
// These are all small objects so capturing by value should be fine
|
||||
const auto thumbnailSize = m_thumbnailSize;
|
||||
const auto counterPtr = &m_processedCount;
|
||||
QFuture<Data*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
|
||||
auto data = Data::create(path, thumbnailSize);
|
||||
const auto cacheDir = m_cacheDir;
|
||||
QFuture<Data*> future =
|
||||
QtConcurrent::mapped(paths, [thumbnailSize, counterPtr, cacheDir](const QString& path) {
|
||||
auto data = Data::create(path, thumbnailSize, cacheDir);
|
||||
counterPtr->fetch_add(1, std::memory_order_relaxed);
|
||||
return data;
|
||||
});
|
||||
@@ -224,7 +231,6 @@ int WallReel::Core::Image::Model::_convertProxyIndex(int proxyIndex) const {
|
||||
|
||||
void WallReel::Core::Image::Model::_clearData() {
|
||||
beginResetModel();
|
||||
m_provider.clear();
|
||||
qDeleteAll(m_data);
|
||||
m_data.clear();
|
||||
for (auto& i : m_sortIndices) {
|
||||
@@ -323,7 +329,6 @@ void WallReel::Core::Image::Model::_onProcessingFinished() {
|
||||
for (auto& data : results) {
|
||||
if (data && data->isValid()) {
|
||||
m_data.append(data);
|
||||
m_provider.insert(data);
|
||||
} else {
|
||||
Logger::warn("Failed to load image: " + (data ? data->getFullPath() : "null"));
|
||||
delete data;
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
#ifndef WALLREEL_IMAGEMODEL_HPP
|
||||
#define WALLREEL_IMAGEMODEL_HPP
|
||||
|
||||
#include <qcontainerfwd.h>
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QDir>
|
||||
#include <QFutureWatcher>
|
||||
#include <QTimer>
|
||||
#include <atomic>
|
||||
|
||||
#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 {
|
||||
|
||||
/**
|
||||
* @brief An unrefactored (view)model class that manages and provides the image list and properties of the focused image.
|
||||
*
|
||||
*/
|
||||
class Model : public QAbstractListModel {
|
||||
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)
|
||||
// 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)
|
||||
// Total count of images to be loaded, used to calculate the progress percentage
|
||||
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(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)
|
||||
|
||||
public:
|
||||
@@ -28,6 +65,7 @@ class Model : public QAbstractListModel {
|
||||
|
||||
enum Roles {
|
||||
IdRole = Qt::UserRole + 1,
|
||||
UrlRole,
|
||||
PathRole,
|
||||
NameRole
|
||||
};
|
||||
@@ -35,6 +73,7 @@ class Model : public QAbstractListModel {
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
return {
|
||||
{IdRole, "imgId"},
|
||||
{UrlRole, "imgUrl"}, // file:///...
|
||||
{PathRole, "imgPath"},
|
||||
{NameRole, "imgName"},
|
||||
};
|
||||
@@ -43,9 +82,9 @@ class Model : public QAbstractListModel {
|
||||
// Constructor / Destructor
|
||||
|
||||
Model(
|
||||
Provider& provider,
|
||||
const Config::SortConfigItems& sortConfig,
|
||||
QSize thumbnailSize,
|
||||
const QDir& cacheDir,
|
||||
const QSize& thumbnailSize,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
~Model();
|
||||
@@ -93,18 +132,25 @@ class Model : public QAbstractListModel {
|
||||
private:
|
||||
int _convertProxyIndex(int proxyIndex) const;
|
||||
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);
|
||||
// Reobtain the properties of the focused image and emit corresponding signals
|
||||
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);
|
||||
|
||||
signals:
|
||||
// Properties
|
||||
void isLoadingChanged();
|
||||
void progressChanged();
|
||||
void totalCountChanged();
|
||||
void currentSortTypeChanged();
|
||||
void currentSortReverseChanged();
|
||||
void currentSortTypeChanged(); // -> _onSortMethodChanged
|
||||
void currentSortReverseChanged(); // -> _onSortMethodChanged
|
||||
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();
|
||||
|
||||
private slots:
|
||||
@@ -114,8 +160,8 @@ class Model : public QAbstractListModel {
|
||||
void _onSearchTextChanged();
|
||||
|
||||
private:
|
||||
Provider& m_provider;
|
||||
const Config::SortConfigItems& m_sortConfig;
|
||||
QDir m_cacheDir;
|
||||
QSize m_thumbnailSize;
|
||||
|
||||
QList<Data*> m_data;
|
||||
@@ -131,9 +177,6 @@ class Model : public QAbstractListModel {
|
||||
// QTimer m_searchDebounceTimer;
|
||||
// static constexpr int s_SearchDebounceIntervalMs = 300;
|
||||
|
||||
QColor m_focusedColor{};
|
||||
QString m_focusedColorName{};
|
||||
|
||||
QFutureWatcher<Data*> m_watcher;
|
||||
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 {
|
||||
return name == other.name;
|
||||
}
|
||||
|
||||
bool isValid() const {
|
||||
return !name.isEmpty() && color.isValid();
|
||||
}
|
||||
};
|
||||
|
||||
struct PaletteItem {
|
||||
@@ -31,6 +35,8 @@ struct PaletteItem {
|
||||
|
||||
public:
|
||||
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;
|
||||
|
||||
Q_INVOKABLE QColor getColor(const QString& colorName) const {
|
||||
@@ -40,9 +46,20 @@ struct PaletteItem {
|
||||
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 {
|
||||
return name == other.name;
|
||||
}
|
||||
|
||||
bool isValid() const {
|
||||
return !name.isEmpty() && !colors.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Palette
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
|
||||
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);
|
||||
|
||||
} // namespace WallReel::Core::Palette
|
||||
|
||||
@@ -72,16 +72,12 @@ void WallReel::Core::Palette::Manager::updateColor() {
|
||||
if (!m_selectedColor.has_value()) {
|
||||
auto cached = imageData->getCachedColor(m_selectedPalette->name);
|
||||
if (cached.has_value()) {
|
||||
auto it = std::find_if(m_selectedPalette->colors.begin(),
|
||||
m_selectedPalette->colors.end(),
|
||||
[&](const ColorItem& item) {
|
||||
return item.name == cached.value();
|
||||
});
|
||||
if (it != m_selectedPalette->colors.end()) {
|
||||
auto found = m_selectedPalette.value().getColorItem(cached.value());
|
||||
if (found.isValid()) {
|
||||
Logger::debug("Using cached color match for image " + imageData->getFileName() +
|
||||
": " + it->name);
|
||||
m_displayColor = it->color;
|
||||
m_displayColorName = it->name;
|
||||
": " + found.name);
|
||||
m_displayColor = found.color;
|
||||
m_displayColorName = found.name;
|
||||
hasResult = true;
|
||||
return;
|
||||
}
|
||||
@@ -89,6 +85,15 @@ void WallReel::Core::Palette::Manager::updateColor() {
|
||||
auto matched = bestMatch(
|
||||
imageData->getDominantColor(),
|
||||
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() + ": " +
|
||||
matched.name);
|
||||
imageData->cacheColor(m_selectedPalette->name, matched.name);
|
||||
|
||||
@@ -20,17 +20,15 @@ class Manager : public QObject {
|
||||
Image::Model& imageModel,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
const QList<PaletteItem>& availablePalettes() const {
|
||||
return m_palettes;
|
||||
}
|
||||
// Properties
|
||||
|
||||
const QColor& color() const {
|
||||
return m_displayColor;
|
||||
}
|
||||
const QList<PaletteItem>& availablePalettes() const { return m_palettes; }
|
||||
|
||||
const QString& colorName() const {
|
||||
return m_displayColorName;
|
||||
}
|
||||
const QColor& color() const { return m_displayColor; }
|
||||
|
||||
const QString& colorName() const { return m_displayColorName; }
|
||||
|
||||
// Setters
|
||||
|
||||
Q_INVOKABLE void setSelectedPalette(const QVariant& paletteVar) {
|
||||
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
||||
@@ -50,14 +48,32 @@ class Manager : public QObject {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return m_displayColor.isValid()
|
||||
? m_displayColor.name()
|
||||
@@ -65,7 +81,7 @@ class Manager : public QObject {
|
||||
}
|
||||
|
||||
public slots:
|
||||
void updateColor();
|
||||
void updateColor(); // <- Image::Model::focusedImageChanged
|
||||
|
||||
signals:
|
||||
void colorChanged();
|
||||
@@ -75,6 +91,7 @@ class Manager : public QObject {
|
||||
Image::Model& m_imageModel;
|
||||
|
||||
QList<PaletteItem> m_palettes;
|
||||
// Null means auto
|
||||
std::optional<PaletteItem> m_selectedPalette = std::nullopt;
|
||||
std::optional<ColorItem> m_selectedColor = std::nullopt;
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
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);
|
||||
|
||||
} // namespace WallReel::Core::Palette
|
||||
|
||||
@@ -3,6 +3,31 @@
|
||||
|
||||
#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 {
|
||||
|
||||
inline const QList<PaletteItem> preDefinedPalettes = {
|
||||
|
||||
@@ -103,8 +103,8 @@ QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVari
|
||||
{"path", imageData.getFullPath()},
|
||||
{"name", imageData.getFileName()},
|
||||
{"size", QString::number(imageData.getSize())},
|
||||
{"width", QString::number(imageData.getImage().width())},
|
||||
{"height", QString::number(imageData.getImage().height())},
|
||||
{"width", QString::number(imageData.getTargetSize().width())},
|
||||
{"height", QString::number(imageData.getTargetSize().height())},
|
||||
{"palette", palette},
|
||||
{"color", color},
|
||||
{"colorHex", hex},
|
||||
|
||||
@@ -87,6 +87,25 @@ inline QString expandPath(const QString& path) {
|
||||
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.
|
||||
*
|
||||
@@ -122,7 +141,12 @@ inline bool checkImageFile(const QString& filePath) {
|
||||
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
|
||||
auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
if (configDir.isEmpty()) {
|
||||
@@ -132,6 +156,21 @@ inline QString getConfigDir() {
|
||||
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
|
||||
|
||||
#endif // WALLREEL_MISC_HPP
|
||||
|
||||
@@ -32,6 +32,21 @@ void AppOptions::printHelp() {
|
||||
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
|
||||
void AppOptions::printError() {
|
||||
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)");
|
||||
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");
|
||||
parser.addOption(quietOption);
|
||||
|
||||
@@ -71,13 +89,18 @@ void AppOptions::parseArgs(QApplication& app) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.isSet(helpOption)) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.isSet(versionOption)) {
|
||||
printVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (parser.isSet(helpOption)) {
|
||||
printHelp();
|
||||
if (parser.isSet(clearCacheOption)) {
|
||||
clearCache();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ class AppOptions {
|
||||
// -h --help
|
||||
void printHelp();
|
||||
|
||||
// -C --clear-cache
|
||||
void clearCache();
|
||||
|
||||
// Print error message and help
|
||||
void printError();
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ Item {
|
||||
id: img
|
||||
|
||||
anchors.fill: parent
|
||||
source: "image://processed/" + model.imgId
|
||||
source: model.imgUrl
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
cache: true
|
||||
|
||||
+2
-6
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "Core/Config/manager.hpp"
|
||||
#include "Core/Image/model.hpp"
|
||||
#include "Core/Image/provider.hpp"
|
||||
#include "Core/Palette/data.hpp"
|
||||
#include "Core/Palette/manager.hpp"
|
||||
#include "Core/Service/manager.hpp"
|
||||
@@ -33,14 +32,11 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
auto* imageProvider = new Image::Provider();
|
||||
engine.addImageProvider(QLatin1String("processed"), imageProvider);
|
||||
|
||||
auto config = new Config::Manager(
|
||||
Utils::getConfigDir(),
|
||||
s_options.appendDirs,
|
||||
s_options.configPath,
|
||||
imageProvider);
|
||||
&engine);
|
||||
qmlRegisterSingletonInstance(
|
||||
COREMODULE_URI,
|
||||
MODULE_VERSION_MAJOR,
|
||||
@@ -49,8 +45,8 @@ int main(int argc, char* argv[]) {
|
||||
config);
|
||||
|
||||
auto imageModel = new Image::Model(
|
||||
*imageProvider,
|
||||
config->getSortConfig(),
|
||||
Utils::getCacheDir(),
|
||||
config->getFocusImageSize(),
|
||||
config);
|
||||
qmlRegisterSingletonInstance(
|
||||
|
||||
Reference in New Issue
Block a user