From c35c0a724ef108a758fc19e050dcc9706873e7d6 Mon Sep 17 00:00:00 2001 From: Uyanide Date: Tue, 5 Aug 2025 12:32:23 +0200 Subject: [PATCH] init --- .clangd | 2 + .gitignore | 6 ++ CMakeLists.txt | 76 ++++++++++++++++++++ config.cpp | 172 ++++++++++++++++++++++++++++++++++++++++++++ config.h | 48 +++++++++++++ images_carousel.cpp | 12 ++++ images_carousel.h | 21 ++++++ images_carousel.ui | 19 +++++ logger.h | 70 ++++++++++++++++++ main.cpp | 35 +++++++++ mainwindow.cpp | 51 +++++++++++++ mainwindow.h | 43 +++++++++++ mainwindow.ui | 113 +++++++++++++++++++++++++++++ 13 files changed, 668 insertions(+) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 config.cpp create mode 100644 config.h create mode 100644 images_carousel.cpp create mode 100644 images_carousel.h create mode 100644 images_carousel.ui create mode 100644 logger.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..0a6c62f --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-mno-direct-extern-access] \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3995bc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.cache +.vscode +build +*.log + +*.user \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..dc0979e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.16) + +project(wallpaper_chooser VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +set(PROJECT_SOURCES + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui +) + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(wallpaper_chooser + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + images_carousel.h images_carousel.cpp images_carousel.ui + config.h config.cpp + logger.h + ) + # Define target properties for Android with Qt 6 as: + # set_property(TARGET wallpaper_chooser APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + # ${CMAKE_CURRENT_SOURCE_DIR}/android) + # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation +else() + if(ANDROID) + add_library(wallpaper_chooser SHARED + ${PROJECT_SOURCES} + ) + # Define properties for Android with Qt 5 after find_package() calls as: + # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") + else() + add_executable(wallpaper_chooser + ${PROJECT_SOURCES} + ) + endif() +endif() + +target_link_libraries(wallpaper_chooser PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) + +target_include_directories(wallpaper_chooser PRIVATE ${CMAKE_CURRENT_LIST_DIR}) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manually though. +if(${QT_VERSION} VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.wallpaper_chooser) +endif() +set_target_properties(wallpaper_chooser PROPERTIES + ${BUNDLE_ID_OPTION} + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +include(GNUInstallDirs) +install(TARGETS wallpaper_chooser + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(wallpaper_chooser) +endif() diff --git a/config.cpp b/config.cpp new file mode 100644 index 0000000..000b326 --- /dev/null +++ b/config.cpp @@ -0,0 +1,172 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 01:34:52 + * @LastEditTime: 2025-08-05 12:17:37 + * @Description: + */ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "logger.h" +using namespace GeneralLogger; + +static QString expandPath(const QString &path); + +const QString Config::s_DefaultConfigFileName = "config.json"; + +Config::Config(const QString &configDir, const QStringList &searchDirs, QObject *parent) : QObject(parent) { + info(QString("Loading configuration from: %1").arg(configDir)); + _loadConfig(configDir + QDir::separator() + s_DefaultConfigFileName); + + info(QString("Additional search directories: %1").arg(searchDirs.join(", "))); + m_configItems.wallpaperDirs.append(searchDirs); + + info("Loading wallpapers ..."); + _loadWallpapers(); +} + +Config::~Config() { +} + +void Config::_loadConfig(const QString &configPath) { + QFile configFile(configPath); + if (!configFile.open(QIODevice::ReadOnly)) { + error(QString("Failed to open config file: %1").arg(configPath)); + return; + } + QByteArray configData = configFile.readAll(); + configFile.close(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(configData); + if (jsonDoc.isNull() || !jsonDoc.isObject()) { + error(QString("Invalid JSON format in config file")); + return; + } + + static const auto parseJsonArray = [](const QJsonObject &obj, const QString &key, QStringList &list) { + if (obj.contains(key) && obj[key].isArray()) { + QJsonArray array = obj[key].toArray(); + for (const QJsonValue &value : array) { + if (value.isString()) { + list.append(::expandPath(value.toString())); + } + } + } else { + warn(QString("Key '%1' not found or not an array in config").arg(key)); + } + }; + + const auto jsonObj = jsonDoc.object(); + if (!jsonObj.contains("wallpaper") || !jsonObj["wallpaper"].isObject()) { + warn("Key 'wallpaper' not fount or not an object in config"); + return; + } + const auto wallpaperObj = jsonObj.value("wallpaper").toObject(); + parseJsonArray(wallpaperObj, "paths", m_configItems.wallpaperPaths); + parseJsonArray(wallpaperObj, "dirs", m_configItems.wallpaperDirs); + parseJsonArray(wallpaperObj, "excludes", m_configItems.wallpaperExcludes); +} + +void Config::_loadWallpapers() { + m_wallpapers.clear(); + + QSet paths; + + info(QString("Loading wallpapers from %1 specified paths").arg(m_configItems.wallpaperPaths.size()), LogIndent::STEP); + for (const QString &path : m_configItems.wallpaperPaths) { + paths.insert(path); + } + + info(QString("Loading wallpapers from %1 specified directories").arg(m_configItems.wallpaperDirs.size()), LogIndent::STEP); + for (const QString &dirPath : m_configItems.wallpaperDirs) { + QDir dir(dirPath); + if (dir.exists()) { + QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot); + for (const QString &file : files) { + QString filePath = dir.filePath(file); + paths.insert(filePath); + } + } else { + warn(QString("Directory '%1' does not exist").arg(dirPath)); + } + } + + info(QString("Excluding %1 specified paths").arg(m_configItems.wallpaperExcludes.size()), LogIndent::STEP); + for (const QString &exclude : m_configItems.wallpaperExcludes) { + paths.remove(exclude); + } + + m_wallpapers.reserve(paths.size()); + for (const QString &path : paths) { + if (isValidImageFile(path)) { + m_wallpapers.append(path); + } + } + info(QString("Found %1 wallpapers").arg(paths.size())); +} + +bool Config::isValidImageFile(const QString &filePath) { + static const QStringList validExtensions = { + ".jpg", + ".jpeg", + ".png", + ".bmp", + ".gif", + ".webp", + ".tiff", + ".avif", + ".heic", + ".heif"}; + + // check if exist + if (!QFile::exists(filePath)) { + warn(QString("File does not exist: %1").arg(filePath)); + return false; + } + // check if normal file + QFileInfo fileInfo(filePath); + if (!fileInfo.isFile() || !fileInfo.isReadable()) { + warn(QString("Invalid file: %1").arg(filePath)); + return false; + } + // check if valid extension + for (const QString &ext : validExtensions) { + if (filePath.endsWith(ext, Qt::CaseInsensitive)) { + return true; + } + } + warn(QString("Unsupported file type: %1").arg(filePath)); + return false; +} + +static QString expandPath(const QString &path) { + QString expandedPath = path; + + if (expandedPath.startsWith("~/")) { + expandedPath.replace(0, 1, QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + } else if (expandedPath == "~") { + expandedPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + } + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QRegularExpression envVarRegex(R"(\$([A-Za-z_][A-Za-z0-9_]*))"); + QRegularExpressionMatchIterator i = envVarRegex.globalMatch(expandedPath); + + while (i.hasNext()) { + QRegularExpressionMatch match = i.next(); + QString varName = match.captured(1); + QString varValue = env.value(varName); + if (!varValue.isEmpty()) { + expandedPath.replace(match.captured(0), varValue); + } + } + + return QDir::cleanPath(expandedPath); +} \ No newline at end of file diff --git a/config.h b/config.h new file mode 100644 index 0000000..e30e890 --- /dev/null +++ b/config.h @@ -0,0 +1,48 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 01:34:52 + * @LastEditTime: 2025-08-05 12:14:04 + * @Description: + */ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include + +#include +#include +#include + +class Config : public QObject { + Q_OBJECT + + public: + Config(const QString& configDir, const QStringList& searchDirs = {}, QObject* parent = nullptr); + + ~Config(); + + static bool isValidImageFile(const QString& filePath); + + [[nodiscard]] const QStringList& getWallpapers() const { return m_wallpapers; } + + [[nodiscard]] qint64 getWallpaperCount() const { return m_wallpapers.size(); } + + static const QString s_DefaultConfigFileName; + + private: + void + _loadConfig(const QString& configPath); + void _loadWallpapers(); + + private: + struct ConfigItems { + QStringList wallpaperPaths; + QStringList wallpaperDirs; + QStringList wallpaperExcludes; + } m_configItems; + + QStringList m_wallpapers; +}; + +#endif // CONFIG_H diff --git a/images_carousel.cpp b/images_carousel.cpp new file mode 100644 index 0000000..5202fc1 --- /dev/null +++ b/images_carousel.cpp @@ -0,0 +1,12 @@ +#include "images_carousel.h" + +#include "ui_images_carousel.h" + +ImagesCarousel::ImagesCarousel(QWidget *parent) : QWidget(parent), + ui(new Ui::ImagesCarousel) { + ui->setupUi(this); +} + +ImagesCarousel::~ImagesCarousel() { + delete ui; +} diff --git a/images_carousel.h b/images_carousel.h new file mode 100644 index 0000000..99235d8 --- /dev/null +++ b/images_carousel.h @@ -0,0 +1,21 @@ +#ifndef IMAGES_CAROUSEL_H +#define IMAGES_CAROUSEL_H + +#include + +namespace Ui { +class ImagesCarousel; +} + +class ImagesCarousel : public QWidget { + Q_OBJECT + + public: + explicit ImagesCarousel(QWidget *parent = nullptr); + ~ImagesCarousel(); + + private: + Ui::ImagesCarousel *ui; +}; + +#endif // IMAGES_CAROUSEL_H diff --git a/images_carousel.ui b/images_carousel.ui new file mode 100644 index 0000000..cb48a7a --- /dev/null +++ b/images_carousel.ui @@ -0,0 +1,19 @@ + + + ImagesCarousel + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + diff --git a/logger.h b/logger.h new file mode 100644 index 0000000..44c500a --- /dev/null +++ b/logger.h @@ -0,0 +1,70 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 10:43:31 + * @LastEditTime: 2025-08-05 11:49:27 + * @Description: + */ +#ifndef GENERAL_LOGGER_H +#define GENERAL_LOGGER_H + +#include +#include + +namespace GeneralLogger { + +inline constexpr const char* colorInfoMsg[]{"\033[32m", "\033[0m", "\033[0m"}; + +enum LogIndent : qint32 { + GENERAL = 0, + STEP = 1, + DETAIL = 2, +}; + +#ifdef GENERAL_LOGGER_DISABLED +#define ENSURE_ENABLED return; +#else +#define ENSURE_ENABLED +extern QTextStream g_logStream; +#endif + +inline void +info(const QString& msg, + const LogIndent indent = GENERAL, + const bool color = true) { + ENSURE_ENABLED + + g_logStream << (color ? "\033[92m" : "") << "[INFO] "; + for (qint32 i = 0; i < indent; i++) g_logStream << " "; + g_logStream << (color ? colorInfoMsg[indent] : "") << msg << (color ? "\033[0m\n" : "\n"); + g_logStream.flush(); +} + +inline void +warn(const QString& msg, + const LogIndent indent = GENERAL, + const bool color = true) { + ENSURE_ENABLED + + g_logStream << (color ? "\033[93m" : "") << "[WARN] "; + for (uint32_t i = 0; i < indent; i++) g_logStream << " "; + g_logStream << (color ? "\033[33m" : "") << msg << (color ? "\033[0m\n" : "\n"); + g_logStream.flush(); +} + +inline void +error(const QString& msg, + const LogIndent indent = GENERAL, + const bool color = true) { + ENSURE_ENABLED + + g_logStream << (color ? "\033[91m" : "") << "[ERROR] "; + for (uint32_t i = 0; i < indent; i++) g_logStream << " "; + g_logStream << (color ? "\033[31m" : "") << msg << (color ? "\033[0m\n" : "\n"); + g_logStream.flush(); +} + +#undef ENSURE_ENABLED + +}; // namespace GeneralLogger + +#endif // GENERAL_LOGGER_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..de2b809 --- /dev/null +++ b/main.cpp @@ -0,0 +1,35 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 00:37:58 + * @LastEditTime: 2025-08-05 12:16:35 + * @Description: + */ +#include + +#include +#include +#include +#include + +#include "logger.h" +#include "mainwindow.h" + +QTextStream GeneralLogger::g_logStream(stderr); + +static QString getConfigDir() { + auto configDir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + if (configDir.isEmpty()) { + configDir = QDir::homePath() + QDir::separator() + ".config" + QDir::separator() + "wallpaper_chooser"; + } + QDir().mkpath(configDir); + return configDir; +} + +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + + MainWindow w(::getConfigDir()); + w.show(); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..fa1aad4 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,51 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 00:37:58 + * @LastEditTime: 2025-08-05 12:18:00 + * @Description: + */ +#include "mainwindow.h" + +#include +#include +#include + +#include "./ui_mainwindow.h" + +MainWindow::MainWindow(const QString &configDir, QWidget *parent) + : QMainWindow(parent), ui(new Ui::MainWindow) { + + m_config = new Config(configDir, {}, this); + + ui->setupUi(this); + _setupUI(); +} + +MainWindow::~MainWindow() { + delete ui; +} + +void MainWindow::_setupUI() { + connect(ui->confirmButton, &QPushButton::clicked, this, &MainWindow::onConfirm); + connect(ui->cancelButton, &QPushButton::clicked, this, &MainWindow::onCancel); + ui->confirmButton->setFocusPolicy(Qt::NoFocus); + ui->cancelButton->setFocusPolicy(Qt::NoFocus); +} + +void MainWindow::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Escape) { + onCancel(); + } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + onConfirm(); + } else { + QMainWindow::keyPressEvent(event); + } +} + +void MainWindow::onConfirm() { + close(); +} + +void MainWindow::onCancel() { + close(); +} \ No newline at end of file diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..91fbf29 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,43 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 00:37:58 + * @LastEditTime: 2025-08-05 12:01:25 + * @Description: + */ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +#include "config.h" + +QT_BEGIN_NAMESPACE + +namespace Ui { +class MainWindow; +} + +QT_END_NAMESPACE + +class MainWindow : public QMainWindow { + Q_OBJECT + + public: + MainWindow(const QString &configDir, QWidget *parent = nullptr); + ~MainWindow(); + + public slots: + void onConfirm(); + void onCancel(); + + protected: + void keyPressEvent(QKeyEvent *event) override; + + private: + void _setupUI(); + + private: + Ui::MainWindow *ui; + Config *m_config; +}; +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..5ac5161 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,113 @@ + + + MainWindow + + + + 0 + 0 + 750 + 500 + + + + + 0 + 0 + + + + + 750 + 500 + + + + + 750 + 500 + + + + MainWindow + + + QMainWindow::DockOption::AllowTabbedDocks|QMainWindow::DockOption::AnimatedDocks + + + + + + + + 0 + 0 + + + + + + + + + 0 + 50 + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + color: #f38ba8 + + + Cancel + + + true + + + + + + + color: #a6e3a1 + + + Confirm + + + true + + + + + + + + + + + + ImagesCarousel + QWidget +
images_carousel.h
+ 1 +
+
+ + +