diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..8ef0dd2 --- /dev/null +++ b/config.example.json @@ -0,0 +1,17 @@ +{ + "wallpaper": { + "paths": [ + "~/Pictures/116327446_p0.jpg" + ], + "dirs": [ + "~/.config/backgrounds" + ], + "excludes": [ + "~/.config/backgrounds/nao-start-crop-adjusted.jpg", + "~/.config/backgrounds/README.md" + ] + }, + "actions": { + "confirm": "~/.scripts/change_wallpaper.sh " + } +} \ No newline at end of file diff --git a/images_carousel.cpp b/images_carousel.cpp index 5202fc1..c59dfb7 100644 --- a/images_carousel.cpp +++ b/images_carousel.cpp @@ -1,12 +1,208 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 01:22:53 + * @LastEditTime: 2025-08-05 16:51:12 + * @Description: + */ #include "images_carousel.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "logger.h" #include "ui_images_carousel.h" -ImagesCarousel::ImagesCarousel(QWidget *parent) : QWidget(parent), - ui(new Ui::ImagesCarousel) { +using namespace GeneralLogger; + +ImagesCarousel::ImagesCarousel(QWidget* parent) + : QWidget(parent), + ui(new Ui::ImagesCarousel), + m_updateTimer(new QTimer(this)), + m_scrollAnimation(nullptr) { ui->setupUi(this); + + connect(m_updateTimer, &QTimer::timeout, this, &ImagesCarousel::updateImages); + m_updateTimer->start(100); } ImagesCarousel::~ImagesCarousel() { delete ui; + for (auto item : m_imageQueue) { + delete item; + } + 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); +} + +ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel) + : m_path(path), m_carousel(carousel) { + setAutoDelete(true); +} + +void ImagesCarousel::addImageToQueue(const ImageData* data) { + QMutexLocker locker(&m_queueMutex); + auto imageItem = new ImageItem(data, + s_itemWidth, + s_itemHeight, + s_itemFocusWidth, + s_itemFocusHeight, + this); + m_imageQueue.enqueue(imageItem); +} + +void ImagesCarousel::updateImages() { + QMutexLocker locker(&m_queueMutex); + + int processCount = 0; + while (!m_imageQueue.isEmpty() && processCount < 5) { + ImageItem* item = m_imageQueue.dequeue(); + ui->scrollAreaWidgetContents->layout()->addWidget(item); + m_loadedImages.append(item); + if (m_loadedImages.size() == 1) { + item->focusImage(); + } else { + item->unfocusImage(); + } + processCount++; + } +} + +void ImageLoader::run() { + auto data = new ImageData(m_path); + QMetaObject::invokeMethod(m_carousel, + "addImageToQueue", + Qt::QueuedConnection, + Q_ARG(const ImageData*, data)); +} + +ImageData::ImageData(const QString& p) : path(p) { + path = p; + if (!pixmap.load(p)) { + warn(QString("Failed to load image from path: %1").arg(p)); + } + // resize in "cover" mode + const QSize targetSize(ImagesCarousel::s_itemWidth, ImagesCarousel::s_itemHeight); + pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + + // Crop to center + int x = (pixmap.width() - targetSize.width()) / 2; + int y = (pixmap.height() - targetSize.height()) / 2; + pixmap = pixmap.copy(x, y, targetSize.width(), targetSize.height()); +} + +void ImagesCarousel::focusNextImage() { + unfocusCurrImage(); + if (m_loadedImages.size() <= 1) return; + m_currentIndex++; + if (m_currentIndex >= m_loadedImages.size()) { + m_currentIndex = 0; + } + focusCurrImage(); +} + +void ImagesCarousel::focusPrevImage() { + if (m_loadedImages.size() <= 1) return; + unfocusCurrImage(); + m_currentIndex--; + if (m_currentIndex < 0) { + m_currentIndex = m_loadedImages.size() - 1; + } + focusCurrImage(); +} + +void ImagesCarousel::unfocusCurrImage() { + m_loadedImages[m_currentIndex]->unfocusImage(); +} + +void ImagesCarousel::focusCurrImage() { + m_loadedImages[m_currentIndex]->focusImage(); + auto hScrollBar = ui->scrollArea->horizontalScrollBar(); + int spacing = ui->scrollAreaWidgetContents->layout()->spacing(); + int centerOffset = (s_itemWidth + spacing) * m_currentIndex + s_itemFocusWidth / 2 - spacing; + int leftOffset = centerOffset - ui->scrollArea->width() / 2; + if (leftOffset < 0) { + leftOffset = 0; + } + + if (m_scrollAnimation) { + m_scrollAnimation->stop(); + delete m_scrollAnimation; + m_scrollAnimation = nullptr; + } + + m_scrollAnimation = new QPropertyAnimation(hScrollBar, "value"); + m_scrollAnimation->setDuration(300); + m_scrollAnimation->setStartValue(hScrollBar->value()); + m_scrollAnimation->setEndValue(leftOffset); + m_scrollAnimation->setEasingCurve(QEasingCurve::OutCubic); + m_scrollAnimation->start(); +} + +ImageItem::ImageItem(const ImageData* data, + const int itemWidth, + const int itemHeight, + const int itemFocusWidth, + const int itemFocusHeight, + QWidget* parent) + : QLabel(parent), + m_data(data), + m_itemSize(itemWidth, itemHeight), + m_itemFocusSize(itemFocusWidth, itemFocusHeight) { + setPixmap(data->pixmap); + setFixedSize(ImagesCarousel::s_itemWidth, ImagesCarousel::s_itemHeight); + setScaledContents(true); +} + +void ImageItem::focusImage() { + if (m_scaleAnimation) { + m_scaleAnimation->stop(); + delete m_scaleAnimation; + m_scaleAnimation = nullptr; + } + m_scaleAnimation = new QPropertyAnimation(this, "size"); + m_scaleAnimation->setDuration(ImagesCarousel::s_animationDuration); + m_scaleAnimation->setStartValue(size()); + m_scaleAnimation->setEndValue(m_itemFocusSize); + m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_scaleAnimation, + &QPropertyAnimation::valueChanged, + this, + [this](const QVariant& value) { + setFixedSize(value.toSize()); + }); + m_scaleAnimation->start(); +} + +void ImageItem::unfocusImage() { + if (m_scaleAnimation) { + m_scaleAnimation->stop(); + delete m_scaleAnimation; + m_scaleAnimation = nullptr; + } + m_scaleAnimation = new QPropertyAnimation(this, "size"); + m_scaleAnimation->setDuration(ImagesCarousel::s_animationDuration); + m_scaleAnimation->setStartValue(size()); + m_scaleAnimation->setEndValue(m_itemSize); + m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic); + connect(m_scaleAnimation, + &QPropertyAnimation::valueChanged, + this, + [this](const QVariant& value) { + setFixedSize(value.toSize()); + }); + m_scaleAnimation->start(); +} \ No newline at end of file diff --git a/images_carousel.h b/images_carousel.h index 99235d8..4f0d8f6 100644 --- a/images_carousel.h +++ b/images_carousel.h @@ -1,8 +1,68 @@ +/* + * @Author: Uyanide pywang0608@foxmail.com + * @Date: 2025-08-05 01:22:53 + * @LastEditTime: 2025-08-05 16:49:22 + * @Description: + */ #ifndef IMAGES_CAROUSEL_H #define IMAGES_CAROUSEL_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +class ImagesCarousel; + +struct ImageData { + QString path; + QPixmap pixmap; + + ImageData() = default; + + explicit ImageData(const QString& p); +}; + +class ImageItem : public QLabel { + Q_OBJECT + + public: + explicit ImageItem(const ImageData* data, + const int itemWidth, + const int itemHeight, + const int itemFocusWidth, + const int itemFocusHeight, + QWidget* parent = nullptr); + + public slots: + void focusImage(); + void unfocusImage(); + + private: + const ImageData* m_data; + QSize m_itemSize; + QSize m_itemFocusSize; + QPropertyAnimation* m_scaleAnimation = nullptr; +}; + +class ImageLoader : public QRunnable { + public: + ImageLoader(const QString& path, ImagesCarousel* carousel); + void run() override; + + private: + QString m_path; + ImagesCarousel* m_carousel; +}; + namespace Ui { class ImagesCarousel; } @@ -11,11 +71,53 @@ class ImagesCarousel : public QWidget { Q_OBJECT public: - explicit ImagesCarousel(QWidget *parent = nullptr); + explicit ImagesCarousel(QWidget* parent = nullptr); ~ImagesCarousel(); + static constexpr int s_itemWidth = 320; + static constexpr int s_itemHeight = 200; + static constexpr int s_itemFocusWidth = 480; + static constexpr int s_itemFocusHeight = 300; + static constexpr int s_animationDuration = 300; + + public slots: + void addImageToQueue(const ImageData* data); + void appendImage(const QString& path); + void focusNextImage(); + void focusPrevImage(); + void unfocusCurrImage(); + void focusCurrImage(); + + private slots: + void updateImages(); + private: - Ui::ImagesCarousel *ui; + Ui::ImagesCarousel* ui; + QMutex m_queueMutex; + QQueue m_imageQueue; + QList m_loadedImages; + QTimer* m_updateTimer; + int m_currentIndex = 0; + QPropertyAnimation* m_scrollAnimation; + + signals: + void imageLoaded(ImageData* imageData); +}; + +class ImagesCarouselScrollArea : public QScrollArea { + Q_OBJECT + + public: + explicit ImagesCarouselScrollArea(QWidget* parent = nullptr) + : QScrollArea(parent) { + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setWidgetResizable(true); + } + + protected: + void keyPressEvent(QKeyEvent* event) override { + event->ignore(); + } }; #endif // IMAGES_CAROUSEL_H diff --git a/images_carousel.ui b/images_carousel.ui index cb48a7a..e4ed7a2 100644 --- a/images_carousel.ui +++ b/images_carousel.ui @@ -13,7 +13,41 @@ Form + + + + + border: none + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 382 + 282 + + + + + + + + + + ImagesCarouselScrollArea + QScrollArea +
images_carousel.h
+ 1 +
+
diff --git a/mainwindow.cpp b/mainwindow.cpp index fa1aad4..8f0ae53 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,7 +1,7 @@ /* * @Author: Uyanide pywang0608@foxmail.com * @Date: 2025-08-05 00:37:58 - * @LastEditTime: 2025-08-05 12:18:00 + * @LastEditTime: 2025-08-05 16:51:52 * @Description: */ #include "mainwindow.h" @@ -30,6 +30,10 @@ void MainWindow::_setupUI() { connect(ui->cancelButton, &QPushButton::clicked, this, &MainWindow::onCancel); ui->confirmButton->setFocusPolicy(Qt::NoFocus); ui->cancelButton->setFocusPolicy(Qt::NoFocus); + + for (const auto &image : m_config->getWallpapers()) { + ui->carousel->appendImage(image); + } } void MainWindow::keyPressEvent(QKeyEvent *event) { @@ -37,6 +41,10 @@ void MainWindow::keyPressEvent(QKeyEvent *event) { onCancel(); } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { onConfirm(); + } else if (event->key() == Qt::Key_Space || event->key() == Qt::Key_Tab || event->key() == Qt::Key_Right) { + ui->carousel->focusNextImage(); + } else if (event->key() == Qt::Key_Left) { + ui->carousel->focusPrevImage(); } else { QMainWindow::keyPressEvent(event); } diff --git a/mainwindow.ui b/mainwindow.ui index 5ac5161..dc218ec 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -69,12 +69,12 @@ - + - color: #f38ba8 + color: #a6e3a1 - Cancel + Confirm true @@ -82,12 +82,12 @@ - + - color: #a6e3a1 + color: #f38ba8 - Confirm + Cancel true