refactor: more comments and minor optimizations

This commit is contained in:
2026-01-15 07:28:52 +01:00
parent 063b8bc430
commit 0c218a1e3c
12 changed files with 275 additions and 83 deletions
-1
View File
@@ -44,4 +44,3 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
files: wallpaper-carousel-linux-x64.tar.gz
generate_release_notes: true
+35 -15
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:34:52
* @LastEditTime: 2026-01-15 03:40:25
* @LastEditTime: 2026-01-15 07:18:46
* @Description: Configuration manager.
*/
#ifndef CONFIG_H
@@ -11,6 +11,26 @@
#include <QString>
#include <QStringList>
// Config entries:
//
// wallpaper.paths array image paths
// wallpaper.dirs array directories to search for images.
// all images in these directories will be added.
// NOT recursive.
// wallpaper.excludes array exclude patterns
//
// action.confirm string command to execute on confirm
//
// style.aspect_ratio number (width / height) of each image
// style.image_width number width of each image
// style.image_focus_width number width of focused image
// style.window_width number fixed window width
// style.window_height number fixed window height
// style.no_loading_screen boolean disable loading screen and load images while updating UI in batches
//
// sort.type string sorting type: "none", "name", "date", "size"
// sort.reverse boolean whether to reverse the sorting order
class Config : public QObject {
Q_OBJECT
@@ -23,33 +43,33 @@ class Config : public QObject {
};
struct WallpaperConfigItems {
QStringList paths;
QStringList dirs;
QStringList excludes;
QStringList paths; // "wallpaper.paths"
QStringList dirs; // "wallpaper.dirs"
QStringList excludes; // "wallpaper.excludes"
};
struct ActionConfigItems {
QString confirm;
QString confirm; // "action.confirm"
};
struct StyleConfigItems {
double aspectRatio = 1.6;
int imageWidth = 320;
int imageFocusWidth = 480;
int windowWidth = 750;
int windowHeight = 500;
bool noLoadingScreen = false;
double aspectRatio = 1.6; // "style.aspect_ratio"
int imageWidth = 320; // "style.image_width"
int imageFocusWidth = 480; // "style.image_focus_width"
int windowWidth = 750; // "style.window_width"
int windowHeight = 500; // "style.window_height"
bool noLoadingScreen = false; // "style.no_loading_screen"
};
struct SortConfigItems {
SortType type = SortType::Name;
bool reverse = false;
SortType type = SortType::Name; // "sort.type"
bool reverse = false; // "sort.reverse"
};
Config(
const QString& configDir,
const QString& configDir, // Fixed, usually "~/.config/wallpaper-carousel"
const QStringList& searchDirs = {},
const QString& configPath = "",
const QString& configPath = "", // Override the default config path
QObject* parent = nullptr);
~Config();
+4 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:32:27
* @LastEditTime: 2026-01-15 00:48:24
* @LastEditTime: 2026-01-15 07:09:29
* @Description: Image item widget for displaying an image.
*/
#include "image_item.h"
@@ -16,6 +16,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
ImageData* data = new ImageData(p);
data->image = new QImage();
// Use QImageReader for better performance
QImageReader reader(p);
if (!reader.canRead()) {
warn(QString("Failed to load image from path: %1").arg(p));
@@ -26,6 +27,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
const QSize targetSize(initWidth, initHeight);
const QSize originalSize = reader.size();
// Scale the image to fit the target size while maintaining aspect ratio
if (originalSize.isValid()) {
double widthRatio = (double)targetSize.width() / originalSize.width();
double heightRatio = (double)targetSize.height() / originalSize.height();
@@ -41,6 +43,7 @@ ImageData* ImageData::create(const QString& p, const int initWidth, const int in
return nullptr;
}
// Crop to target size if necessary
if (data->image->size() != targetSize) {
int x = (data->image->width() - targetSize.width()) / 2;
int y = (data->image->height() - targetSize.height()) / 2;
+11 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:31:15
* @LastEditTime: 2026-01-14 23:32:58
* @LastEditTime: 2026-01-15 07:10:21
* @Description: Image item widget for displaying an image.
*/
#ifndef IMAGE_ITEM_H
@@ -13,6 +13,9 @@
#include <QLabel>
#include <QPropertyAnimation>
class ImageData;
class ImageItem;
/**
* @brief Data structure to hold image information
* and can be safely created and passed between threads.
@@ -26,6 +29,7 @@ class ImageData {
~ImageData() { releaseImage(); }
// Optimization: release image data as soon as they are no longer needed
void releaseImage() { delete image, image = nullptr; }
[[nodiscard]] const QImage& getImage() const { return *image; }
@@ -67,6 +71,12 @@ class ImageItem : public QLabel {
[[nodiscard]] qint64 getFileSize() const { return m_data->getFileInfo().size(); }
/**
* @brief Set focus state by scaling the image label
*
* @param focus whether to focus
* @param animate whether to animate the transition
*/
void setFocus(bool focus = true, bool animate = true);
protected:
+15 -13
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2026-01-15 05:11:52
* @LastEditTime: 2026-01-15 07:24:52
* @Description: Animated carousel widget for displaying and selecting images.
*/
#include "images_carousel.h"
@@ -50,11 +50,6 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
this,
&ImagesCarousel::_onImagesLoaded);
connect(this,
&ImagesCarousel::stopped,
this,
&ImagesCarousel::_onImagesLoaded);
// Auto focus when scrolling
m_scrollDebounceTimer = new QTimer(this);
m_scrollDebounceTimer->setSingleShot(true);
@@ -78,6 +73,12 @@ ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
}
void ImagesCarousel::_onImagesLoaded() {
// reset stop sign
{
// No need to lock m_countMutex here, but just for safety
QMutexLocker locker(&m_stopSignMutex);
m_stopSign = false;
}
m_animationEnabled = true;
if (!m_noLoadingScreen) {
_enableUIUpdates(true);
@@ -86,7 +87,6 @@ void ImagesCarousel::_onImagesLoaded() {
m_imageInsertQueueTimer->deleteLater();
m_imageInsertQueueTimer = nullptr;
}
if (m_initialImagesLoaded) {
// No images loaded
if (!getLoadedImagesCount()) {
return;
@@ -94,12 +94,11 @@ void ImagesCarousel::_onImagesLoaded() {
// Focus the first image
if (m_currentIndex < 0) {
m_currentIndex = 0;
// Ensure the layout events are processed
// Ensure the layout events are processed before focusing
QTimer::singleShot(0, this, [this]() {
focusCurrImage();
});
}
}
// exit(1); // for debug
}
@@ -156,7 +155,7 @@ void ImagesCarousel::_insertImageQueue(ImageData* data) {
}
{
QMutexLocker locker(&m_imageInsertQueueMutex);
m_imageInsertQueue.enqueue(const_cast<ImageData*>(data));
m_imageInsertQueue.enqueue(data);
}
}
@@ -166,15 +165,16 @@ int ImagesCarousel::_insertImage(ImageData* data) {
emit imageLoaded(getLoadedImagesCount());
{
QMutexLocker countLocker(&m_countMutex);
if (++m_loadedImagesCount >= m_addedImagesCount) {
if (++m_processedImagesCount >= m_addedImagesCount) {
{
QMutexLocker stopSignLocker(&m_stopSignMutex);
if (m_stopSign) {
// if all stopped
emit stopped();
} else {
emit loadingCompleted(m_loadedImagesCount);
}
}
emit loadingCompleted(m_processedImagesCount);
}
}
return;
});
@@ -241,6 +241,7 @@ void ImagesCarousel::_processImageInsertQueue() {
int currPos = m_currentIndex;
for (ImageData* data : batch) {
int pos = _insertImage(data);
// Keep the focusing index correct
if (pos >= 0 && pos <= currPos) {
currPos++;
}
@@ -276,6 +277,7 @@ void ImageLoader::run() {
Qt::QueuedConnection,
Q_ARG(ImageData*, data));
});
// We need to call _insertImageQueue even if stopped to increase the loaded count
{
QMutexLocker stopSignLocker(&m_carousel->m_stopSignMutex);
if (m_carousel->m_stopSign) return;
+85 -13
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2026-01-15 03:42:17
* @LastEditTime: 2026-01-15 07:06:46
* @Description: Animated carousel widget for displaying and selecting images.
*/
#ifndef IMAGES_CAROUSEL_H
@@ -20,8 +20,57 @@
#include "config.h"
#include "image_item.h"
class ImageData;
class ImageItem;
// Two different image loading strategies:
// - With loading screen: load all images directly
// 1. appendImages called -> increace m_addedImagesCount & spawn all ImageLoader
// threads
// 2. Each ImageLoader calls _insertImageQueue with queued connection
// 3. _insertImageQueue calls _insertImage directly
// - Without loading screen: queue loaded images and insert them in batches
// 1. appendImages called -> increace m_addedImagesCount & spawn all ImageLoader
// threads and a timer m_imageInsertQueueTimer, disable UI updates
// 2. Each ImageLoader calls _insertImageQueue with queued connection
// 3. _insertImageQueue enqueues the ImageData
// 4. m_imageInsertQueueTimer calls _processImageInsertQueue every
// s_processBatchTimeout ms
// 5. _processImageInsertQueue processes up to s_processBatchSize items from the
// queue and calls _insertImage for each
//
// The stop logic is identical:
// - Force stop
// 1. Set m_stopSign to true
// 2. ImageLoader::run checks m_stopSign and returns early if true
// and calls _insertImageQueue using queued connection, but passing a
// nullptr as parameter
// 3. The callstack from _insertImageQueue to _insertImage is same as above
// 4. _insertImage ignores nullptr and just increases m_processedImagesCount
// 5. when m_processedImagesCount >= m_addedImagesCount, emit stopped()
// 6. Call ImagesCarousel::_onImagesLoaded
// - Normal completion
// 1. Same as above until _insertImage, but m_stopSign is false and ImageLoader::run
// passes valid ImageData pointer to _insertImageQueue
// 2. When m_processedImagesCount >= m_addedImagesCount, emit loadingCompleted()
// 3. Call ImagesCarousel::_onImagesLoaded
//
// 3 different ways to change focusing image:
// - focusNextImage / focusPrevImage: directly change m_currentIndex and call
// focusCurrImage
// These can be triggered by different events, e.g. key press, button click, etc.
// - Auto focus on scroll: debounce scroll events and calculate the nearest image
// index to focus, then change m_currentIndex and call focusCurrImage
// - Initial focus: set m_currentIndex to 0 and call focusCurrImage
//
// Note:
// - All methods and slots of ImageCarousel should be called from the main thread.
// - ImageCarousel::m_addedImagesCount and ImageCarousel::m_processedImagesCount
// should be identical after loading is finished, regardless of whether loading is
// forcedly stopped or completed normally.
// - ImageCarousel::getLoadedImagesCount() returns the number of images currently
// displayed in the carousel, which may be less than m_addedImagesCount if loading
// is not yet completed or some images failed to load.
// - The current implementation actually supports dynamic addition of images during runtime,
// but the UI does not provide such functionality yet and thus it is not tested :)
class ImageLoader;
class ImagesCarousel;
class ImagesCarouselScrollArea;
@@ -56,11 +105,19 @@ class ImagesCarousel : public QWidget {
QWidget* parent = nullptr);
~ImagesCarousel();
static constexpr int s_debounceInterval = 200;
static constexpr int s_animationDuration = 300;
static constexpr int s_debounceInterval = 200; // ms
static constexpr int s_animationDuration = 300; // ms
static constexpr int s_processBatchTimeout = 50; // ms
static constexpr int s_processBatchSize = 30; // items
/**
* @brief Get the Current Image Path
*
* @return QString
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] QString getCurrentImagePath() const {
if (m_currentIndex >= 0 && m_currentIndex < getLoadedImagesCount()) {
auto item = getImageItemAt(m_currentIndex);
@@ -71,11 +128,25 @@ class ImagesCarousel : public QWidget {
return "";
}
// Should always be called in the main thread
/**
* @brief Get count of loaded images
*
* @return qsizetype
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] qsizetype getLoadedImagesCount() const {
return m_imagesLayout->count();
}
/**
* @brief Get the Image object at index
*
* @param index
* @return ImageItem*
*
* @note This method should be always called from the main thread.
*/
[[nodiscard]] ImageItem* getImageItemAt(int index) const {
if (index < 0 || index >= getLoadedImagesCount()) {
return nullptr;
@@ -86,6 +157,11 @@ class ImagesCarousel : public QWidget {
->widget());
}
/**
* @brief Get count of added images
*
* @return qsizetype
*/
[[nodiscard]] qsizetype getAddedImagesCount() {
QMutexLocker locker(&m_countMutex);
return m_addedImagesCount;
@@ -110,8 +186,7 @@ class ImagesCarousel : public QWidget {
private slots:
void _onScrollBarValueChanged(int value);
void _onItemClicked(const QString& path);
void _onImagesLoaded();
void _onImagesLoaded(); // Called when loading is completed or stopped
void _processImageInsertQueue();
public:
@@ -131,9 +206,9 @@ class ImagesCarousel : public QWidget {
ImagesCarouselScrollArea* m_scrollArea = nullptr;
// Items and counters
int m_loadedImagesCount = 0; // increase when _insertImage is called OR ImageLoader::run() is called with m_stopSign as true
int m_processedImagesCount = 0; // increase when _insertImage is called OR ImageLoader::run() is called with m_stopSign as true
int m_addedImagesCount = 0; // increase when appendImages called
QMutex m_countMutex; // for m_loadedImagesCount and m_addedImagesCount
QMutex m_countMutex; // for m_processedImagesCount and m_addedImagesCount
int m_currentIndex = -1; // initially no focus
// Threading
@@ -154,9 +229,6 @@ class ImagesCarousel : public QWidget {
QMutex m_stopSignMutex;
bool m_stopSign = false;
// Flags
bool m_initialImagesLoaded = true;
signals:
void imageFocused(const QString& path, const int index, const int count);
+5 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-07 01:12:37
* @LastEditTime: 2026-01-15 04:07:40
* @LastEditTime: 2026-01-15 06:26:35
* @Description: Implementation of logger.
*/
#include "logger.h"
@@ -23,6 +23,7 @@ static QTextStream* s_logStream = nullptr;
static bool s_isColored = false;
static QMutex s_logMutex;
// Check if the output stream supports colored output
static bool checkIsColored(FILE* stream) {
if (!stream || !isatty(fileno(stream))) {
return false;
@@ -35,6 +36,7 @@ static bool checkIsColored(FILE* stream) {
return true;
}
// Custom message handler
static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
Q_UNUSED(context);
@@ -126,3 +128,5 @@ void GeneralLogger::warn(const QString& msg) {
void GeneralLogger::critical(const QString& msg) {
qCCritical(logMain).noquote() << msg;
}
// No fatal because qCFatal does not exist before Qt 6.5
+15 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 10:43:31
* @LastEditTime: 2026-01-15 04:07:46
* @LastEditTime: 2026-01-15 06:25:57
* @Description: A simple thread-safe logger.
*/
#ifndef GENERAL_LOGGER_H
@@ -27,10 +27,24 @@ void critical(const QString& msg);
class Logger {
public:
/**
* @brief Initialize the logger and set the output stream.
*
* @param stream
*/
static void init(FILE* stream = stderr);
/**
* @brief Set the log level.
*
* @param level
*/
static void setLogLevel(QtMsgType level);
/**
* @brief Suppress all log output.
*
*/
static void quiet();
};
+18 -2
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2026-01-15 03:41:55
* @LastEditTime: 2026-01-15 05:58:06
* @Description: Argument parser and entry point.
*/
#include <QApplication>
@@ -16,21 +16,32 @@
#include "utils.h"
#include "version.h"
/**
* @brief A static & single-instance class to handle application options.
*
*/
static class AppOptions {
QCommandLineParser parser{};
// The following 3 functions handle specific command line options
// and mark doReturn as true to indicate that the application should exit
// after parsing arguments.
// -v --version
void printVersion() {
QTextStream out(stdout);
out << APP_NAME << " version " << APP_VERSION << Qt::endl;
doReturn = true;
}
// -h --help
void printHelp() {
QTextStream out(stdout);
out << parser.helpText() << Qt::endl;
doReturn = true;
}
// Print error message and help
void printError() {
if (!errorText.isEmpty()) {
QTextStream out(stderr);
@@ -44,7 +55,7 @@ static class AppOptions {
QString configPath = "";
QStringList appendDirs;
QString errorText = "";
bool doReturn = false;
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
void parseArgs(QApplication* a) {
parser.setApplicationDescription("A small wallpaper utility made with Qt");
@@ -64,6 +75,9 @@ static class AppOptions {
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
parser.addOption(configFileOption);
// Not parser.process(a->arguments()) because we want to handle exit logics ourselves.
// parser.process(...) will do something like exit(...) that will terminate
// the application brutally and produce unwanted warnings.
if (!parser.parse(a->arguments())) {
errorText = parser.errorText();
doReturn = true;
@@ -85,6 +99,7 @@ static class AppOptions {
} else if (parser.isSet(quietOption)) {
Logger::quiet();
} else {
// Default to INFO level
Logger::setLogLevel(QtInfoMsg);
}
@@ -113,6 +128,7 @@ static class AppOptions {
} s_options;
static QString 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()) {
configDir = QDir::homePath() + QDir::separator() + ".config" + QDir::separator() + APP_NAME;
+22 -17
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2026-01-15 05:30:06
* @LastEditTime: 2026-01-15 07:25:41
* @Description: MainWindow implementation.
*/
#include "main_window.h"
@@ -15,14 +15,10 @@
#include "./ui_main_window.h"
#include "images_carousel.h"
#include "logger.h"
#include "utils.h"
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);
@@ -85,6 +81,7 @@ void MainWindow::_setupUI() {
}
void MainWindow::keyPressEvent(QKeyEvent* event) {
// Same effects as clicking the confirm/cancel buttons
if (event->key() == Qt::Key_Escape) {
_onCancelPressed();
return;
@@ -136,6 +133,8 @@ void MainWindow::keyPressEvent(QKeyEvent* event) {
}
}
// Stop loading images and call onStopped when loading is really stopped
// emit stop() -> ImagesCarousel::onStop() -> ImagesCarousel::stopped() -> call onStopped
void MainWindow::_stopLoadingAndQuit(const std::function<void()>& onStopped) {
if (m_state != Loading) {
return;
@@ -170,8 +169,7 @@ void MainWindow::_onCancelPressed() {
debug("Stopping loading and displaying loaded images...");
_stopLoadingAndQuit([this]() {
debug("Loading stopped.");
_onLoadingCompleted(m_carousel->getLoadedImagesCount());
m_carousel->focusCurrImage();
// and do nothing
});
}
break;
@@ -190,13 +188,13 @@ void MainWindow::_onConfirmPressed() {
// case loading screen is disabled, confirm the selection
if (m_config.getStyleConfig().noLoadingScreen) {
debug("Stopping loading and confirming selection...");
connect(
m_carousel,
&ImagesCarousel::stopped,
this,
&MainWindow::onConfirm);
m_state = Stopping;
emit stop();
// Save current path because the stopping process may take some time
const QString currentPath = m_carousel->getCurrentImagePath();
debug("Loading stopped. Confirming selection...");
_stopLoadingAndQuit([this, currentPath]() {
close();
_runConfirmAction(currentPath);
});
}
break;
case Ready:
@@ -213,6 +211,7 @@ void MainWindow::wheelEvent(QWheelEvent* event) {
event->ignore();
return;
}
// angleDelta().x() is handled by QScrollArea
if (event->angleDelta().y() > 0) {
m_carousel->focusPrevImage();
} else if (event->angleDelta().y() < 0) {
@@ -226,7 +225,7 @@ void MainWindow::closeEvent(QCloseEvent* event) {
if (m_state == Loading) {
event->ignore();
_stopLoadingAndQuit([this]() {
debug("Quitting app.");
debug("Quitting app...");
close();
});
} else {
@@ -236,12 +235,16 @@ void MainWindow::closeEvent(QCloseEvent* event) {
void MainWindow::onConfirm() {
close();
const auto path = m_carousel->getCurrentImagePath();
_runConfirmAction(m_carousel->getCurrentImagePath());
}
void MainWindow::_runConfirmAction(const QString& path) {
if (path.isEmpty()) {
warn("No image selected");
return;
}
info(QString("Selected image: %1").arg(path));
// Output the selected path to stdout
QTextStream out(stdout);
out << path << Qt::endl;
const auto cmdOrig = m_config.getActionConfig().confirm;
@@ -270,11 +273,13 @@ void MainWindow::_onLoadingStarted(const qsizetype amount) {
return;
}
m_loadingIndicator->setMaximum(amount);
// Change to loading indicator view
ui->stackedWidget->setCurrentIndex(m_loadingIndicatorIndex);
}
void MainWindow::_onLoadingCompleted(const qsizetype amount) {
info(QString("Loading completed, loaded %1 images").arg(amount));
// Change to carousel view
ui->stackedWidget->setCurrentIndex(m_carouselIndex);
m_state = Ready;
}
+6 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2025-12-01 00:37:35
* @LastEditTime: 2026-01-15 06:16:07
* @Description: MainWindow implementation.
*/
#ifndef MAINWINDOW_H
@@ -39,6 +39,7 @@ class MainWindow : public QMainWindow {
private:
void _setupUI();
void _stopLoadingAndQuit(const std::function<void()>& onStopped = nullptr);
void _runConfirmAction(const QString& path = "");
private slots:
void _onImageFocused(const QString& path, const int index, const int count);
@@ -56,6 +57,10 @@ class MainWindow : public QMainWindow {
Ready,
} m_state = Init;
// Init -> Loading -> Ready -> (Quit)
// Init -> Loading -> Stopping -> Ready -> (Quit) (with loading screen)
// Init -> Loading -> Stopping -> (Quit) (without loading screen)
Ui::MainWindow* ui;
ImagesCarousel* m_carousel = nullptr;
LoadingIndicator* m_loadingIndicator = nullptr;
+43 -1
View File
@@ -1,7 +1,7 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:59:57
* @LastEditTime: 2026-01-15 03:11:08
* @LastEditTime: 2026-01-15 06:00:51
* @Description: THE utils header that every project needs :)
*/
@@ -15,6 +15,11 @@
#include <QStandardPaths>
#include <utility>
/**
* @brief Defer execution of a callable until the end of the current scope.
*
* @tparam Callable
*/
template <typename Callable>
class Defer {
Callable m_func;
@@ -31,16 +36,34 @@ class Defer {
}
};
/**
* @brief Check if a file exists, is a regular file, and is readable.
*
* @param path
*/
inline bool checkFile(const QString& path) {
QFileInfo checkFile(path);
// According to Qt docs, "exists() returns true if the symlink points to an existing target, otherwise it returns false."
// So no need to separately check for isSymbolicLink() or isSymLink().
return checkFile.exists() && checkFile.isFile() && checkFile.isReadable();
}
/**
* @brief Check if a directory exists, is a directory, and is readable.
*
* @param path
*/
inline bool checkDir(const QString& path) {
QFileInfo checkFile(path);
return checkFile.exists() && checkFile.isDir() && checkFile.isReadable();
}
/**
* @brief Expand environment variables and ~ in a given path.
*
* @param path Input path
* @return QString Expanded path
*/
inline QString expandPath(const QString& path) {
QString expandedPath = path;
@@ -66,6 +89,25 @@ inline QString expandPath(const QString& path) {
return QDir::cleanPath(expandedPath);
}
/**
* @brief Split the file name from a given path.
*
* @param path
* @return QString
*/
static QString splitNameFromPath(const QString& path) {
QFileInfo fileInfo(path);
return fileInfo.fileName();
}
/**
* @brief In addition to checking if the file exists and is readable,
* also checks if the file has a valid image extension.
*
* @param filePath
* @return true
* @return false
*/
inline bool checkImageFile(const QString& filePath) {
static const QStringList validExtensions = {
".jpg",