wip: functioning
This commit is contained in:
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:34:52
|
||||
* @LastEditTime: 2025-08-05 17:26:33
|
||||
* @Description: Configuration manager.
|
||||
*/
|
||||
#include "config.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#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");
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
if (!jsonObj.contains("actions") || !jsonObj["actions"].isObject()) {
|
||||
warn("Key 'actions' not found or not an object in config");
|
||||
} else {
|
||||
const auto actionsObj = jsonObj.value("actions").toObject();
|
||||
if (actionsObj.contains("confirm") && actionsObj["confirm"].isString()) {
|
||||
m_configItems.actionsConfirm = ::expandPath(actionsObj["confirm"].toString());
|
||||
info(QString("Action on confirm: %1").arg(m_configItems.actionsConfirm));
|
||||
} else {
|
||||
warn("Key 'confirm' not found or not a string in 'actions'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Config::_loadWallpapers() {
|
||||
m_wallpapers.clear();
|
||||
|
||||
QSet<QString> 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);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:34:52
|
||||
* @LastEditTime: 2025-08-05 17:26:25
|
||||
* @Description: Configuration manager.
|
||||
*/
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
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(); }
|
||||
|
||||
[[nodiscard]] const QString& getActionsConfirm() const { return m_configItems.actionsConfirm; }
|
||||
|
||||
static const QString s_DefaultConfigFileName;
|
||||
|
||||
private:
|
||||
void
|
||||
_loadConfig(const QString& configPath);
|
||||
void _loadWallpapers();
|
||||
|
||||
private:
|
||||
struct ConfigItems {
|
||||
QStringList wallpaperPaths;
|
||||
QStringList wallpaperDirs;
|
||||
QStringList wallpaperExcludes;
|
||||
QString actionsConfirm;
|
||||
} m_configItems;
|
||||
|
||||
QStringList m_wallpapers;
|
||||
};
|
||||
|
||||
#endif // CONFIG_H
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ImagesCarousel</class>
|
||||
<widget class="QWidget" name="ImagesCarousel">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="ImagesCarouselScrollArea" name="scrollArea">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">border: none</string>
|
||||
</property>
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>382</width>
|
||||
<height>282</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2"/>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ImagesCarouselScrollArea</class>
|
||||
<extends>QScrollArea</extends>
|
||||
<header>images_carousel.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>750</width>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>750</width>
|
||||
<height>500</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>750</width>
|
||||
<height>500</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<property name="dockOptions">
|
||||
<set>QMainWindow::DockOption::AllowTabbedDocks|QMainWindow::DockOption::AnimatedDocks</set>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="ImagesCarousel" name="carousel" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="actions" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>50</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="confirmButton">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #a6e3a1</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Confirm</string>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: #f38ba8</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ImagesCarousel</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>images_carousel.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:22:53
|
||||
* @LastEditTime: 2025-08-05 17:25:59
|
||||
* @Description: Animated carousel widget for displaying and selecting images.
|
||||
*/
|
||||
#include "images_carousel.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <qevent.h>
|
||||
#include <qpropertyanimation.h>
|
||||
#include <qvariantanimation.h>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QMetaObject>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
|
||||
#include "logger.h"
|
||||
#include "ui_images_carousel.h"
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:22:53
|
||||
* @LastEditTime: 2025-08-05 17:25:34
|
||||
* @Description: Animated carousel widget for displaying and selecting images.
|
||||
*/
|
||||
#ifndef IMAGES_CAROUSEL_H
|
||||
#define IMAGES_CAROUSEL_H
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QPixmap>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QQueue>
|
||||
#include <QRunnable>
|
||||
#include <QScrollArea>
|
||||
#include <QThreadPool>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
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);
|
||||
|
||||
[[nodiscard]] const QString& getPath() const { return m_data->path; }
|
||||
|
||||
[[nodiscard]] const QPixmap& getPixmap() const { return m_data->pixmap; }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class ImagesCarousel : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
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;
|
||||
|
||||
[[nodiscard]] QString getCurrentImagePath() const {
|
||||
if (m_currentIndex < 0 || m_currentIndex >= m_loadedImages.size()) {
|
||||
return "";
|
||||
}
|
||||
return m_loadedImages[m_currentIndex]->getPath();
|
||||
}
|
||||
|
||||
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;
|
||||
QMutex m_queueMutex;
|
||||
QQueue<ImageItem*> m_imageQueue;
|
||||
QList<ImageItem*> m_loadedImages;
|
||||
QTimer* m_updateTimer;
|
||||
int m_currentIndex = 0;
|
||||
QPropertyAnimation* m_scrollAnimation;
|
||||
};
|
||||
|
||||
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
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 10:43:31
|
||||
* @LastEditTime: 2025-08-05 17:25:18
|
||||
* @Description: A simple logger for general use.
|
||||
* g_logStream must be defined somewhere else in the project.
|
||||
*/
|
||||
#ifndef GENERAL_LOGGER_H
|
||||
#define GENERAL_LOGGER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QTextStream>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 00:37:58
|
||||
* @LastEditTime: 2025-08-05 17:34:37
|
||||
* @Description: Entry point.
|
||||
*/
|
||||
#include <qapplication.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "logger.h"
|
||||
#include "main_window.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();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 00:37:58
|
||||
* @LastEditTime: 2025-08-05 17:40:35
|
||||
* @Description: MainWindow implementation.
|
||||
*/
|
||||
#include "main_window.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QKeyEvent>
|
||||
#include <QProcess>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "./ui_main_window.h"
|
||||
#include "images_carousel.h"
|
||||
#include "logger.h"
|
||||
|
||||
using namespace GeneralLogger;
|
||||
|
||||
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);
|
||||
|
||||
for (const auto &image : m_config->getWallpapers()) {
|
||||
ui->carousel->appendImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onConfirm() {
|
||||
close();
|
||||
const auto path = ui->carousel->getCurrentImagePath();
|
||||
if (path.isEmpty()) {
|
||||
warn("No image selected");
|
||||
return;
|
||||
}
|
||||
info(QString("Selected image: %1").arg(path));
|
||||
const auto cmdOrig = m_config->getActionsConfirm();
|
||||
if (cmdOrig.isEmpty()) {
|
||||
warn("No action defined for confirmation");
|
||||
return;
|
||||
}
|
||||
const auto cmd = cmdOrig.arg(path);
|
||||
info(QString("Executing command: %1").arg(cmd));
|
||||
|
||||
const auto arguments = QProcess::splitCommand(cmd);
|
||||
|
||||
if (QProcess::execute(arguments.first(), arguments.mid(1))) {
|
||||
error(QString("Failed to execute command: %1").arg(cmd));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onCancel() {
|
||||
close();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 00:37:58
|
||||
* @LastEditTime: 2025-08-05 17:23:41
|
||||
* @Description: MainWindow implementation.
|
||||
*/
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
#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
|
||||
Reference in New Issue
Block a user