diff --git a/config.example.json b/config.example.json index ed298e3..eb29998 100644 --- a/config.example.json +++ b/config.example.json @@ -21,5 +21,9 @@ "image_focus_width": 480, "window_width": 750, "window_height": 500 + }, + "sort": { + "type": "size", + "reverse": false } } \ No newline at end of file diff --git a/src/config.cpp b/src/config.cpp index dd27b36..cd07cab 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,7 +1,7 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:34:52 - * @LastEditTime: 2025-08-05 20:11:11 + * @LastEditTime: 2025-08-06 00:23:54 * @Description: Configuration manager. */ #include "config.h" @@ -99,6 +99,29 @@ void Config::_loadConfig(const QString &configPath) { info(QString("Window height: %1").arg(m_configItems.styleWindowHeight)); } }}, + {"sort.type", "type", [this](const QJsonValue &val) { + if (val.isString()) { + QString type = val.toString().toLower(); + if (type == "none") { + m_configItems.sortType = SortType::None; + } else if (type == "name") { + m_configItems.sortType = SortType::Name; + } else if (type == "date") { + m_configItems.sortType = SortType::Date; + } else if (type == "size") { + m_configItems.sortType = SortType::Size; + } else { + warn(QString("Unknown sort type: %1").arg(type)); + } + } + info(QString("Sort type: %1").arg(static_cast(m_configItems.sortType))); + }}, + {"sort.reverse", "reverse", [this](const QJsonValue &val) { + if (val.isBool()) { + m_configItems.sortReverse = val.toBool(); + info(QString("Sort reverse: %1").arg(m_configItems.sortReverse)); + } + }}, }; // 统一解析 @@ -180,6 +203,7 @@ void Config::_loadWallpapers() { m_wallpapers.append(path); } } + info(QString("Found %1 wallpapers").arg(paths.size())); } diff --git a/src/config.h b/src/config.h index dde5534..5666bc7 100644 --- a/src/config.h +++ b/src/config.h @@ -1,7 +1,7 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:34:52 - * @LastEditTime: 2025-08-05 20:12:47 + * @LastEditTime: 2025-08-06 00:23:45 * @Description: Configuration manager. */ #ifndef CONFIG_H @@ -15,6 +15,13 @@ class Config : public QObject { Q_OBJECT public: + enum class SortType : int { + None = 0, + Name, + Date, + Size, + }; + Config(const QString& configDir, const QStringList& searchDirs = {}, QObject* parent = nullptr); ~Config(); @@ -37,6 +44,10 @@ class Config : public QObject { [[nodiscard]] int getStyleWindowHeight() const { return m_configItems.styleWindowHeight; } + [[nodiscard]] SortType getSortType() const { return m_configItems.sortType; } + + [[nodiscard]] bool isSortReverse() const { return m_configItems.sortReverse; } + static const QString s_DefaultConfigFileName; private: @@ -53,8 +64,10 @@ class Config : public QObject { double styleAspectRatio = 1.6; int styleImageWidth = 320; int styleImageFocusWidth = 480; - int styleWindowWidth = 800; - int styleWindowHeight = 600; + int styleWindowWidth = 720; + int styleWindowHeight = 500; + SortType sortType = SortType::None; + bool sortReverse = false; } m_configItems; QStringList m_wallpapers; diff --git a/src/designer/main_window.ui b/src/designer/main_window.ui index 8cbfa79..9e38a54 100644 --- a/src/designer/main_window.ui +++ b/src/designer/main_window.ui @@ -36,6 +36,38 @@ + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + + + Qt::AlignmentFlag::AlignCenter + + + diff --git a/src/images_carousel.cpp b/src/images_carousel.cpp index 5e481c1..7dbca19 100644 --- a/src/images_carousel.cpp +++ b/src/images_carousel.cpp @@ -1,20 +1,20 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 - * @LastEditTime: 2025-08-05 20:06:23 + * @LastEditTime: 2025-08-06 00:47:21 * @Description: Animated carousel widget for displaying and selecting images. */ #include "images_carousel.h" #include -#include -#include -#include +#include #include #include #include #include +#include +#include #include "logger.h" #include "ui_images_carousel.h" @@ -24,6 +24,8 @@ using namespace GeneralLogger; ImagesCarousel::ImagesCarousel(const double itemAspectRatio, const int itemWidth, const int itemFocusWidth, + const Config::SortType sortType, + const bool sortReverse, QWidget* parent) : QWidget(parent), ui(new Ui::ImagesCarousel), @@ -32,11 +34,18 @@ ImagesCarousel::ImagesCarousel(const double itemAspectRatio, m_itemWidth(itemWidth), m_itemHeight(static_cast(itemWidth / itemAspectRatio)), m_itemFocusWidth(itemFocusWidth), - m_itemFocusHeight(static_cast(itemFocusWidth / itemAspectRatio)) { + m_itemFocusHeight(static_cast(itemFocusWidth / itemAspectRatio)), + m_sortType(sortType), + m_sortReverse(sortReverse) { ui->setupUi(this); + m_imagesLayout = dynamic_cast(ui->scrollAreaWidgetContents->layout()); connect(m_updateTimer, &QTimer::timeout, this, &ImagesCarousel::_updateImages); m_updateTimer->start(100); + + connect(this, &ImagesCarousel::imagesLoaded, this, [this]() { + _focusCurrImage(); + }); } ImagesCarousel::~ImagesCarousel() { @@ -44,15 +53,23 @@ ImagesCarousel::~ImagesCarousel() { for (auto item : std::as_const(m_imageQueue)) { delete item; } + // memory of items in m_loadedImages managed by Qt parent-child system + // ... if (m_scrollAnimation) { m_scrollAnimation->stop(); delete m_scrollAnimation; } } -void ImagesCarousel::appendImage(const QString& path) { - ImageLoader* loader = new ImageLoader(path, this); - QThreadPool::globalInstance()->start(loader); +void ImagesCarousel::appendImages(const QStringList& paths) { + { + QMutexLocker locker(&m_imageCountMutex); + m_imageCount += paths.size(); + } + for (const QString& path : paths) { + ImageLoader* loader = new ImageLoader(path, this); + QThreadPool::globalInstance()->start(loader); + } } ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel) @@ -78,19 +95,45 @@ void ImagesCarousel::_addImageToQueue(const ImageData* data) { void ImagesCarousel::_updateImages() { QMutexLocker locker(&m_queueMutex); + static const QVector> cmpFuncs = { + [](auto, auto) { + return false; + }, // None + [](auto a, auto b) { + return a->getFileName() < b->getFileName(); + }, + [](auto a, auto b) { + return a->getFileDate() < b->getFileDate(); + }, + [](auto a, auto b) { + return a->getFileSize() < b->getFileSize(); + }, + }; + int processCount = 0; while (!m_imageQueue.isEmpty() && processCount < 5) { ImageItem* item = m_imageQueue.dequeue(); - ui->scrollAreaWidgetContents->layout()->addWidget(item); - m_loadedImages.append(item); - // focus first image - if (m_loadedImages.size() == 1) { - item->setFocus(true); - } else { - item->setFocus(false); + + // insert into correct position based on sort type and direction + // currently O(n^2), but better as O(n * (n + log(n))) with vector and binary search + qint64 inserPos = m_loadedImages.size(); + if (m_sortType != Config::SortType::None) { + for (auto it = m_loadedImages.rbegin(); + it != m_loadedImages.rend() && + cmpFuncs[static_cast(m_sortType)](*it, item) == m_sortReverse; + ++it, --inserPos); } + m_loadedImages.insert(inserPos, item); + m_imagesLayout->insertWidget(inserPos, item); processCount++; } + + { + QMutexLocker countLocker(&m_imageCountMutex); + if (m_loadedImages.size() >= m_imageCount) { + emit imagesLoaded(); + } + } } void ImageLoader::run() { @@ -101,8 +144,7 @@ void ImageLoader::run() { Q_ARG(const ImageData*, data)); } -ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : path(p) { - path = p; +ImageData::ImageData(const QString& p, const int initWidth, const int initHeight) : file(p) { if (!pixmap.load(p)) { warn(QString("Failed to load image from path: %1").arg(p)); } @@ -137,11 +179,16 @@ void ImagesCarousel::focusPrevImage() { } void ImagesCarousel::_unfocusCurrImage() { + // bound check was (or should) done by caller m_loadedImages[m_currentIndex]->setFocus(false); } void ImagesCarousel::_focusCurrImage() { + // bound check was (or should) done by caller m_loadedImages[m_currentIndex]->setFocus(true); + emit imageFocused(m_loadedImages[m_currentIndex]->getFileFullPath(), + m_currentIndex, + m_loadedImages.size()); auto hScrollBar = ui->scrollArea->horizontalScrollBar(); int spacing = ui->scrollAreaWidgetContents->layout()->spacing(); int centerOffset = (m_itemWidth + spacing) * m_currentIndex + m_itemFocusWidth / 2 - spacing; diff --git a/src/images_carousel.h b/src/images_carousel.h index 6fae25e..06ef5ae 100644 --- a/src/images_carousel.h +++ b/src/images_carousel.h @@ -1,14 +1,14 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 01:22:53 - * @LastEditTime: 2025-08-05 19:47:43 + * @LastEditTime: 2025-08-06 00:47:12 * @Description: Animated carousel widget for displaying and selecting images. */ #ifndef IMAGES_CAROUSEL_H #define IMAGES_CAROUSEL_H -#include - +#include +#include #include #include #include @@ -22,6 +22,8 @@ #include #include +#include "config.h" + class ImagesCarousel; /** @@ -29,7 +31,7 @@ class ImagesCarousel; * and can be safely created and passed between threads. */ struct ImageData { - QString path; + QFileInfo file; QPixmap pixmap; explicit ImageData(const QString& p, const int initWidth, const int initHeight); @@ -51,10 +53,16 @@ class ImageItem : public QLabel { ~ImageItem() override; - [[nodiscard]] const QString& getPath() const { return m_data->path; } + [[nodiscard]] QString getFileFullPath() const { return m_data->file.absoluteFilePath(); } + + [[nodiscard]] QString getFileName() const { return m_data->file.fileName(); } + + [[nodiscard]] QDateTime getFileDate() const { return m_data->file.lastModified(); } [[nodiscard]] const QPixmap& getPixmap() const { return m_data->pixmap; } + [[nodiscard]] qint64 getFileSize() const { return m_data->file.size(); } + public: void setFocus(bool focus = true); @@ -90,6 +98,8 @@ class ImagesCarousel : public QWidget { explicit ImagesCarousel(const double itemAspectRatio, const int itemWidth, const int itemFocusWidth, + const Config::SortType sortType, + const bool sortReverse, QWidget* parent = nullptr); ~ImagesCarousel(); @@ -99,13 +109,15 @@ class ImagesCarousel : public QWidget { if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) { return ""; } - return m_loadedImages[m_currentIndex]->getPath(); + return m_loadedImages[m_currentIndex]->getFileFullPath(); } - const int m_itemWidth = 320; - const int m_itemHeight = 180; - const int m_itemFocusWidth = 480; - const int m_itemFocusHeight = 270; + const int m_itemWidth = 320; + const int m_itemHeight = 180; + const int m_itemFocusWidth = 480; + const int m_itemFocusHeight = 270; + const Config::SortType m_sortType = Config::SortType::None; + const bool m_sortReverse = false; public slots: void focusNextImage(); @@ -115,7 +127,7 @@ class ImagesCarousel : public QWidget { void _unfocusCurrImage(); public: - void appendImage(const QString& path); + void appendImages(const QStringList& paths); private: Q_INVOKABLE void _addImageToQueue(const ImageData* data); @@ -130,6 +142,13 @@ class ImagesCarousel : public QWidget { QTimer* m_updateTimer; int m_currentIndex = 0; QPropertyAnimation* m_scrollAnimation; + QHBoxLayout* m_imagesLayout = nullptr; + QMutex m_imageCountMutex; + int m_imageCount = 0; + + signals: + void imageFocused(const QString& path, const int index, const int count); + void imagesLoaded(); }; class ImagesCarouselScrollArea : public QScrollArea { diff --git a/src/main_window.cpp b/src/main_window.cpp index 19c266a..947440a 100644 --- a/src/main_window.cpp +++ b/src/main_window.cpp @@ -1,7 +1,7 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 00:37:58 - * @LastEditTime: 2025-08-05 20:12:40 + * @LastEditTime: 2025-08-06 00:48:11 * @Description: MainWindow implementation. */ #include "main_window.h" @@ -17,6 +17,11 @@ using namespace GeneralLogger; +static QString splitNameFromPath(const QString &path) { + QFileInfo fileInfo(path); + return fileInfo.fileName(); +} + MainWindow::MainWindow(const Config &config, QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_config(config) { ui->setupUi(this); @@ -33,8 +38,11 @@ void MainWindow::_setupUI() { m_config.getStyleAspectRatio(), m_config.getStyleImageWidth(), m_config.getStyleImageFocusWidth(), + m_config.getSortType(), + m_config.isSortReverse(), this); - ui->mainLayout->insertWidget(0, m_carousel); + ui->mainLayout->insertWidget(2, m_carousel); + connect(m_carousel, &ImagesCarousel::imageFocused, this, &MainWindow::_onImageFocused); // set window size setMinimumSize(m_config.getStyleWindowWidth(), m_config.getStyleWindowHeight()); @@ -45,9 +53,7 @@ void MainWindow::_setupUI() { ui->confirmButton->setFocusPolicy(Qt::NoFocus); ui->cancelButton->setFocusPolicy(Qt::NoFocus); - for (const auto &image : m_config.getWallpapers()) { - m_carousel->appendImage(image); - } + m_carousel->appendImages(m_config.getWallpapers()); } void MainWindow::keyPressEvent(QKeyEvent *event) { @@ -90,4 +96,8 @@ void MainWindow::onConfirm() { void MainWindow::onCancel() { close(); +} + +void MainWindow::_onImageFocused(const QString &path, const int index, const int count) { + ui->topLabel->setText(QString("%1 (%2/%3)").arg(splitNameFromPath(path)).arg(index + 1).arg(count)); } \ No newline at end of file diff --git a/src/main_window.h b/src/main_window.h index 90776c6..b28b9fc 100644 --- a/src/main_window.h +++ b/src/main_window.h @@ -1,7 +1,7 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 00:37:58 - * @LastEditTime: 2025-08-05 19:53:51 + * @LastEditTime: 2025-08-06 00:47:04 * @Description: MainWindow implementation. */ #ifndef MAINWINDOW_H @@ -37,6 +37,9 @@ class MainWindow : public QMainWindow { private: void _setupUI(); + private slots: + void _onImageFocused(const QString &path, const int index, const int count); + private: Ui::MainWindow *ui; ImagesCarousel *m_carousel = nullptr;