This commit is contained in:
2026-02-16 14:22:03 +01:00
parent ad742a38bd
commit 9e088c2024
38 changed files with 934 additions and 1973 deletions
-46
View File
@@ -1,46 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake qt6-base-dev
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release -- -j$(nproc)
- name: Package
run: |
mkdir -p package/bin
mkdir -p package/share/applications
cp build/wallpaper-carousel package/bin/
cp README.md package/
cp LICENSE package/
cp app/wallpaper-carousel.desktop package/share/applications/
cd package
tar -czvf ../wallpaper-carousel-linux-x64.tar.gz *
- name: Create Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: wallpaper-carousel-linux-x64.tar.gz
+82 -7
View File
@@ -1,8 +1,83 @@
.cache # This file is used to ignore files which are generated
.vscode # ----------------------------------------------------------------------------
.qtcreator
build-dbg
build
*.log
*.user *~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
*.qbs.user*
CMakeLists.txt.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe
# Directories with generated files
.moc/
.obj/
.pch/
.rcc/
.uic/
/build*/
.cache
+37 -66
View File
@@ -1,18 +1,15 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(Wallpaper_Carousel VERSION 1.0.3 LANGUAGES CXX) project(Wallpaper_Carousel VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME "wallpaper-carousel") set(EXECUTABLE_NAME "wallreel")
set(CORELIB_NAME "wallreel-core")
set(UILIB_NAME "wallreel-ui")
set(COREMODULE_URI "WallReel.Core")
set(UIMODULE_URI "WallReel.UI")
set(MODULE_VERSION_MAJOR 1)
set(MODULE_VERSION_MINOR 0)
configure_file(src/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC_SEARCH_PATHS src/designer)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -20,72 +17,50 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release) set(CMAKE_BUILD_TYPE Release)
endif() endif()
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) if(CMAKE_BUILD_TYPE STREQUAL "Release")
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) add_definitions(-DSUPPRESS_QRC_LOG)
set(PROJECT_SOURCES
src/main.cpp
src/main_window.cpp
src/main_window.h
src/designer/main_window.ui
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(${EXECUTABLE_NAME}
MANUAL_FINALIZATION
${PROJECT_SOURCES}
src/utils.h
src/images_carousel.h src/images_carousel.cpp src/designer/images_carousel.ui
src/config.h src/config.cpp
src/logger.h src/logger.cpp
src/loading_indicator.h src/loading_indicator.cpp src/designer/loading_indicator.ui
src/image_item.h src/image_item.cpp
)
# 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(${EXECUTABLE_NAME} 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(${EXECUTABLE_NAME}
${PROJECT_SOURCES}
)
endif()
endif() endif()
target_link_libraries(${EXECUTABLE_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) configure_file(src/version.h.in ${CMAKE_BINARY_DIR}/generated/version.h)
target_include_directories(${EXECUTABLE_NAME} PRIVATE src ${CMAKE_BINARY_DIR}/generated) find_package(Qt6 REQUIRED COMPONENTS Quick Widgets QuickControls2 Concurrent)
# if(NOT ${CMAKE_BUILD_TYPE} STREQUAL "Debug") qt_standard_project_setup(REQUIRES 6.5)
# target_compile_definitions(wallpaper_chooser PRIVATE
# GENERAL_LOGGER_DISABLED qt_policy(SET QTP0004 NEW)
# )
# endif() add_subdirectory(src/core)
add_subdirectory(src/ui)
add_executable(${EXECUTABLE_NAME}
src/main.cpp
)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # 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 # If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though. # 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(${EXECUTABLE_NAME} PROPERTIES set_target_properties(${EXECUTABLE_NAME} PROPERTIES
${BUNDLE_ID_OPTION}
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.wallpaper-carousel
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE WIN32_EXECUTABLE TRUE
) )
target_link_libraries(${EXECUTABLE_NAME} PRIVATE
Qt6::Quick
Qt6::Widgets
Qt6::QuickControls2
Qt6::Concurrent
${CORELIB_NAME}
${UILIB_NAME}
)
target_include_directories(${EXECUTABLE_NAME} PRIVATE
${CMAKE_BINARY_DIR}/generated
)
include(GNUInstallDirs) include(GNUInstallDirs)
install(TARGETS ${EXECUTABLE_NAME} install(TARGETS ${EXECUTABLE_NAME}
BUNDLE DESTINATION . BUNDLE DESTINATION .
@@ -96,7 +71,3 @@ install(TARGETS ${EXECUTABLE_NAME}
install(FILES ${CMAKE_CURRENT_LIST_DIR}/app/${EXECUTABLE_NAME}.desktop install(FILES ${CMAKE_CURRENT_LIST_DIR}/app/${EXECUTABLE_NAME}.desktop
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
) )
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(${EXECUTABLE_NAME})
endif()
-7
View File
@@ -1,7 +0,0 @@
Copyright 2026 Uyanide pywang0608@foxmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-95
View File
@@ -1,95 +0,0 @@
## What is this
It might not be that worthy to write a QtWidget application for such a small feature, but I kind of enjoy the pain... So here it is.
<img src="https://github.com/Uyanide/backgrounds/blob/master/screenshots/desktop-alt.jpg?raw=true"/>
## How to build
1. Make sure you have Qt6 libraries, CMake and a C++ compiler installed.
e.g. On Arch-based systems:
```bash
sudo pacman -S --needed qt6-base cmake gcc
```
2. Clone the repository:
```bash
git clone https://github.com/Uyanide/Wallpaper_Carousel.git --depth 1 && \
cd Wallpaper_Carousel
```
3. Run [build script](app/install.sh):
```bash
app/install.sh
```
or if you prefer a prefix other than `/usr/local`, e.g. `$HOME/.local`:
```bash
PREFIX=$HOME/.local ./app/install.sh
```
> [!Warning]
>
> This script will ask for root permissions if the prefix is set to a system directory like `/usr/local`. Please make sure you have read and trust the script before proceeding.
## How to use
The config file should be placed in `~/.config/wallpaper-carousel/config.json`. Refer to [config.example.json](config.example.json) and [config.h](src/config.h) for specific entries.
A minimum config should at least contain the path(s) to wallpapers, e.g.
```json
{
"wallpaper": {
"dirs": ["/path/to/your/wallpapers"]
}
}
```
By default, the path of the selected wallpaper will be output to stdout. If you want to apply the selected wallpaper automatically after selection, the `action.confirm` entry should be set, e.g.
```json
{
"wallpaper": {
"dirs": ["/path/to/your/wallpapers"]
},
"action": {
"confirm": "awww img \"%1\""
}
}
```
`action.confirm` should be a executable followed by a couple of arguments, where `%1` will be replaced by the path of the selected wallpaper.
## CLI
```
Usage: wallpaper-carousel [options]
Options:
-h, --help Displays help on commandline options.
-v, --version Displays version information.
-V, --verbose Set log level to DEBUG (default is INFO)
-q, --quiet Suppress all log output
-d, --append-dir <dir> Append an additional wallpaper search directory
-c, --config-file <file> Specify a custom configuration file
```
A few things to notice:
- It's generally not necessary to provide any CLI arguments, I would recommend using the config file to customize the behavior instead. However, it is still possible to control some essential options via CLI.
- All logs are directed to stderr by default. Only the full path of the selected wallpaper (if any) will be sent to stdout. This allows easy piping of the output to other programs.
- The `--append-dir` option can be used multiple times to add multiple directories.
- It is quite obvious that some options are conflicting with each other (e.g. `--verbose` and `--quiet`). If mutually exclusive options are provided together, the behavior is undefined and can be changed without notice in future versions.
- Paths passed via CLI options are tested before any further operation is performed. That is to say, if an invalid path is provided, the program will exit with an error before any further action, and you won't even have a chance to see a window.
On the contrary, paths provided in the config file are only tested when they are actually used (e.g. when searching for wallpapers). And most errors will be ignored silently (with a warning log).
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
prefix=${PREFIX:-/usr/local}
path="$(dirname "$(readlink -f "$0")")"
cmake -S "$path/.." -B "$path/../build" \
-DCMAKE_INSTALL_PREFIX="$prefix"
cmake --build "$path/../build" --config Release -- -j"$(nproc)"
if [ ! -w "$prefix" ] && [ "$(id -u)" -ne 0 ]; then
echo "Elevated permissions are required to install to $prefix, enter root's password to continue."
su -m -c "cmake --install '$path/../build' --config Release" root
else
cmake --install "$path/../build" --config Release
fi
-12
View File
@@ -1,12 +0,0 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Wallpaper Carousel
GenericName=Animated wallpaper selector
TryExec=wallpaper-carousel
Exec=wallpaper-carousel
Comment=A small wallpaper utility made with Qt
Terminal=false
Categories=Application;Utility;DesktopSettings;
StartupNotify=true
Keywords=wallpapers;
-31
View File
@@ -1,31 +0,0 @@
{
"wallpaper": {
"paths": [
"~/Pictures/116327446_p0.jpg"
],
"dirs": [
"~/Pictures/backgrounds",
"/media/Beta/壁纸/库"
],
"excludes": [
"~/.config/backgrounds/nao-stars-crop-adjust-flop.jpg",
"~/.config/backgrounds/miku-gate.jpg",
"~/.config/backgrounds/README.md"
]
},
"action": {
"confirm": "change-wallpaper \"%1\""
},
"style": {
"aspect_ratio": 1.6,
"image_width": 320,
"image_focus_width": 480,
"window_width": 750,
"window_height": 500,
"no_loading_screen": false
},
"sort": {
"type": "date",
"reverse": true
}
}
+22
View File
@@ -0,0 +1,22 @@
qt_add_qml_module(${CORELIB_NAME}
URI ${COREMODULE_URI}
VERSION ${MODULE_VERSION_MAJOR}.${MODULE_VERSION_MINOR}
SOURCES
imagedata.hpp imagedata.cpp
utils/logger.hpp utils/logger.cpp
utils/misc.hpp
configmgr.hpp configmgr.cpp
imagemodel.hpp imagemodel.cpp
imageprovider.hpp imageprovider.cpp
)
target_link_libraries(${CORELIB_NAME} PRIVATE
Qt6::Quick
Qt6::Widgets
Qt6::QuickControls2
Qt6::Concurrent
)
target_include_directories(${CORELIB_NAME} PRIVATE
${CMAKE_BINARY_DIR}/generated
)
+7 -19
View File
@@ -1,10 +1,4 @@
/* #include "configmgr.hpp"
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:34:52
* @LastEditTime: 2026-01-15 03:54:42
* @Description: Configuration manager.
*/
#include "config.h"
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
@@ -14,8 +8,8 @@
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QStandardPaths> #include <QStandardPaths>
#include "logger.h" #include "utils/logger.hpp"
#include "utils.h" #include "utils/misc.hpp"
using namespace GeneralLogger; using namespace GeneralLogger;
const QString Config::s_DefaultConfigFileName = "config.json"; const QString Config::s_DefaultConfigFileName = "config.json";
@@ -125,12 +119,6 @@ void Config::_loadConfig(const QString& configPath) {
debug(QString("Window height: %1").arg(m_styleConfig.windowHeight)); debug(QString("Window height: %1").arg(m_styleConfig.windowHeight));
} }
}}, }},
{"style.no_loading_screen", "no_loading_screen", [this](const QJsonValue& val) {
if (val.isBool()) {
m_styleConfig.noLoadingScreen = val.toBool();
debug(QString("No loading screen: %1").arg(m_styleConfig.noLoadingScreen));
}
}},
{"sort.type", "type", [this](const QJsonValue& val) { {"sort.type", "type", [this](const QJsonValue& val) {
if (val.isString()) { if (val.isString()) {
QString type = val.toString().toLower(); QString type = val.toString().toLower();
@@ -188,16 +176,16 @@ void Config::_loadWallpapers() {
QSet<QString> paths; QSet<QString> paths;
debug(QString("Loading wallpapers from %1 specified paths...").arg(m_wallpaperConfig.paths.size())); debug(QString("Loading wallpapers from %1 specified paths...").arg(m_wallpaperConfig.paths.size()));
for (const QString& path : m_wallpaperConfig.paths) { for (const QString& path : std::as_const(m_wallpaperConfig.paths)) {
paths.insert(path); paths.insert(path);
} }
debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size())); debug(QString("Loading wallpapers from %1 specified directories...").arg(m_wallpaperConfig.dirs.size()));
for (const QString& dirPath : m_wallpaperConfig.dirs) { for (const QString& dirPath : std::as_const(m_wallpaperConfig.dirs)) {
QDir dir(dirPath); QDir dir(dirPath);
if (checkDir(dirPath)) { if (checkDir(dirPath)) {
QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot); QStringList files = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
for (const QString& file : files) { for (const QString& file : std::as_const(files)) {
QString filePath = dir.filePath(file); QString filePath = dir.filePath(file);
paths.insert(expandPath(filePath)); paths.insert(expandPath(filePath));
} }
@@ -207,7 +195,7 @@ void Config::_loadWallpapers() {
} }
debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size())); debug(QString("Excluding %1 specified paths...").arg(m_wallpaperConfig.excludes.size()));
for (const QString& exclude : m_wallpaperConfig.excludes) { for (const QString& exclude : std::as_const(m_wallpaperConfig.excludes)) {
paths.remove(exclude); paths.remove(exclude);
} }
+33 -17
View File
@@ -1,13 +1,9 @@
/* #ifndef WALLREEL_CONFIGMGR_HPP
* @Author: Uyanide pywang0608@foxmail.com #define WALLREEL_CONFIGMGR_HPP
* @Date: 2025-08-05 01:34:52
* @LastEditTime: 2026-01-15 07:18:46
* @Description: Configuration manager.
*/
#ifndef CONFIG_H
#define CONFIG_H
#include <QObject> #include <QObject>
#include <QQmlEngine>
#include <QSize>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
@@ -26,7 +22,6 @@
// style.image_focus_width number width of focused image // style.image_focus_width number width of focused image
// style.window_width number fixed window width // style.window_width number fixed window width
// style.window_height number fixed window height // 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.type string sorting type: "none", "name", "date", "size"
// sort.reverse boolean whether to reverse the sorting order // sort.reverse boolean whether to reverse the sorting order
@@ -34,6 +29,12 @@
class Config : public QObject { class Config : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(double aspectRatio READ getAspectRatio CONSTANT)
Q_PROPERTY(int imageWidth READ getImageWidth CONSTANT)
Q_PROPERTY(int imageFocusWidth READ getImageFocusWidth CONSTANT)
Q_PROPERTY(int windowWidth READ getWindowWidth CONSTANT)
Q_PROPERTY(int windowHeight READ getWindowHeight CONSTANT)
public: public:
enum class SortType : int { enum class SortType : int {
None = 0, // "none" None = 0, // "none"
@@ -58,7 +59,6 @@ class Config : public QObject {
int imageFocusWidth = 480; // "style.image_focus_width" int imageFocusWidth = 480; // "style.image_focus_width"
int windowWidth = 750; // "style.window_width" int windowWidth = 750; // "style.window_width"
int windowHeight = 500; // "style.window_height" int windowHeight = 500; // "style.window_height"
bool noLoadingScreen = false; // "style.no_loading_screen"
}; };
struct SortConfigItems { struct SortConfigItems {
@@ -74,17 +74,33 @@ class Config : public QObject {
~Config(); ~Config();
[[nodiscard]] const QStringList& getWallpapers() const { return m_wallpapers; } const QStringList& getWallpapers() const { return m_wallpapers; }
[[nodiscard]] qint64 getWallpaperCount() const { return m_wallpapers.size(); } qint64 getWallpaperCount() const { return m_wallpapers.size(); }
[[nodiscard]] const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; } const WallpaperConfigItems& getWallpaperConfig() const { return m_wallpaperConfig; }
[[nodiscard]] const ActionConfigItems& getActionConfig() const { return m_actionConfig; } const ActionConfigItems& getActionConfig() const { return m_actionConfig; }
[[nodiscard]] const StyleConfigItems& getStyleConfig() const { return m_styleConfig; } const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
[[nodiscard]] const SortConfigItems& getSortConfig() const { return m_sortConfig; } const SortConfigItems& getSortConfig() const { return m_sortConfig; }
double getAspectRatio() const { return m_styleConfig.aspectRatio; }
int getImageWidth() const { return m_styleConfig.imageWidth; }
int getImageFocusWidth() const { return m_styleConfig.imageFocusWidth; }
int getWindowWidth() const { return m_styleConfig.windowWidth; }
int getWindowHeight() const { return m_styleConfig.windowHeight; }
QSize getFocusImageSize() const {
int width = m_styleConfig.imageFocusWidth;
int height = static_cast<int>(width / m_styleConfig.aspectRatio);
return {width, height};
}
static const QString s_DefaultConfigFileName; static const QString s_DefaultConfigFileName;
const QString m_configDir; const QString m_configDir;
@@ -102,4 +118,4 @@ class Config : public QObject {
QStringList m_wallpapers; QStringList m_wallpapers;
}; };
#endif // CONFIG_H #endif // WALLREEL_CONFIGMGR_HPP
+64
View File
@@ -0,0 +1,64 @@
#include "imagedata.hpp"
#include <QImageReader>
#include "utils/logger.hpp"
using namespace GeneralLogger;
ImageData* ImageData::create(const QString& path, const QSize& size) {
ImageData* ret = new ImageData(path, size);
if (!ret->isValid()) {
delete ret;
return nullptr;
}
return ret;
}
ImageData::ImageData(const QString& path, const QSize& targetSize)
: file(path) {
QImageReader reader(path);
if (!reader.canRead()) {
warn(QString("Failed to load image from path: %1").arg(path));
return;
}
const QSize originalSize = reader.size();
// Scale the image to fit the target size while maintaining aspect ratio
QSize processSize = originalSize;
if (originalSize.isValid()) {
double widthRatio = (double)targetSize.width() / originalSize.width();
double heightRatio = (double)targetSize.height() / originalSize.height();
double scaleFactor = std::max(widthRatio, heightRatio);
processSize = originalSize * scaleFactor;
if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
reader.setScaledSize(processSize);
}
}
if (!reader.read(&image)) {
warn(QString("Failed to load image from path: %1").arg(path));
return;
}
if (image.width() > processSize.width() || image.height() > processSize.height()) {
image = image.scaled(processSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
// Crop to target size if necessary
if (image.size() != targetSize) {
int x = (image.width() - targetSize.width()) / 2;
int y = (image.height() - targetSize.height()) / 2;
image = image.copy(x, y, targetSize.width(), targetSize.height());
}
// Convert to GPU-friendly format
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
// Create ID
id = QString::number(qHash(file.absoluteFilePath()));
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef WALLREEL_IMAGEDATA_HPP
#define WALLREEL_IMAGEDATA_HPP
#include <QFileInfo>
#include <QImage>
class ImageData {
QString id;
QFileInfo file;
QImage image;
ImageData(const QString& path, const QSize& size);
public:
static ImageData* create(const QString& path, const QSize& size);
const QImage& getImage() const { return image; }
const QString& getId() const { return id; }
bool isValid() const { return !image.isNull(); }
QString getFullPath() const { return file.absoluteFilePath(); }
QString getFileName() const { return file.fileName(); }
QDateTime getLastModified() const { return file.lastModified(); }
qint64 getSize() const { return file.size(); }
const QFileInfo& getFileInfo() const { return file; }
private:
};
#endif // WALLREEL_IMAGEDATA_HPP
+171
View File
@@ -0,0 +1,171 @@
#include "imagemodel.hpp"
#include <QFuture>
#include <QtConcurrent>
#include "imagedata.hpp"
ImageModel::ImageModel(
ImageProvider* provider,
const Config::SortConfigItems& sortConfig,
QSize thumbnailSize,
QObject* parent)
: QAbstractListModel(parent),
m_provider(provider),
m_sortConfig(sortConfig),
m_thumbnailSize(thumbnailSize) {
connect(
&m_watcher,
&QFutureWatcher<ImageData*>::finished,
this,
&ImageModel::onProcessingFinished);
connect(
&m_progressUpdateTimer,
&QTimer::timeout,
this,
[this]() {
emit progressChanged();
});
}
ImageModel::~ImageModel() {
m_watcher.cancel();
m_watcher.waitForFinished();
qDeleteAll(m_data);
}
int ImageModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
return 0;
}
return m_data.count();
}
QVariant ImageModel::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.row() >= m_data.count()) {
return QVariant();
}
const auto& item = m_data[index.row()];
switch (role) {
case IdRole:
return item->getId();
case PathRole:
return item->getFullPath();
case NameRole:
return item->getFileName();
default:
return QVariant();
}
}
void ImageModel::loadAndProcess(const QStringList& paths) {
if (m_isLoading) {
return;
}
m_isLoading = true;
emit isLoadingChanged();
beginResetModel();
if (!m_data.isEmpty()) {
qDeleteAll(m_data);
}
m_data.clear();
m_provider->clear();
endResetModel();
m_processedCount = 0;
m_progressUpdateTimer.start(s_progressUpdateInterval);
const auto thumbnailSize = m_thumbnailSize;
const auto counterPtr = &m_processedCount;
QFuture<ImageData*> future = QtConcurrent::mapped(paths, [thumbnailSize, counterPtr](const QString& path) {
auto data = ImageData::create(path, thumbnailSize);
counterPtr->fetch_add(1, std::memory_order_relaxed);
return data;
});
m_watcher.setFuture(future);
emit totalCountChanged();
}
void ImageModel::stop() {
if (m_isLoading) {
m_watcher.cancel();
}
}
void ImageModel::onProgressValueChanged(int value) {
Q_UNUSED(value);
emit progressChanged();
}
void ImageModel::onProcessingFinished() {
auto results = m_watcher.future().results();
for (auto& data : results) {
if (data && data->isValid()) {
m_data.append(data);
} else {
delete data;
data = nullptr;
}
}
sortUpdate();
m_isLoading = false;
m_progressUpdateTimer.stop();
emit progressChanged();
// emit isLoadingChanged();
QTimer::singleShot(s_isLoadingUpdateInterval, this, [this]() {
emit isLoadingChanged();
});
}
void ImageModel::sortUpdate() {
const auto type = m_sortConfig.type;
const auto reverse = m_sortConfig.reverse;
std::sort(m_data.begin(), m_data.end(), [type, reverse](ImageData* a, ImageData* b) {
if (!a || !b) {
return false;
}
bool result = false;
switch (type) {
case Config::SortType::Name:
result = QString::compare(a->getFileName(), b->getFileName(), Qt::CaseInsensitive) < 0;
break;
case Config::SortType::Date:
result = a->getLastModified() < b->getLastModified();
break;
case Config::SortType::Size:
result = a->getSize() < b->getSize();
break;
default:
break;
}
return reverse ? !result : result;
});
beginResetModel();
m_provider->clear();
for (const auto& item : m_data) {
m_provider->insert(item);
}
endResetModel();
}
QVariant ImageModel::dataAt(int index, const QString& roleName) const {
if (index < 0 || index >= m_data.count()) {
return QVariant();
}
const auto& item = m_data[index];
if (roleName == "imgId") {
return item->getId();
} else if (roleName == "imgPath") {
return item->getFullPath();
} else if (roleName == "imgName") {
return item->getFileName();
} else {
return QVariant();
}
}
+86
View File
@@ -0,0 +1,86 @@
#ifndef WALLREEL_IMAGEMODEL_HPP
#define WALLREEL_IMAGEMODEL_HPP
#include <QAbstractListModel>
#include <QFutureWatcher>
#include <QTimer>
#include <atomic>
#include "configmgr.hpp"
#include "imageprovider.hpp"
class ImageModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(bool isLoading READ isLoading NOTIFY isLoadingChanged)
Q_PROPERTY(int processedCount READ processedCount NOTIFY progressChanged)
Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged)
public:
enum Roles {
IdRole = Qt::UserRole + 1,
PathRole,
NameRole
};
QHash<int, QByteArray> roleNames() const override {
return {
{IdRole, "imgId"},
{PathRole, "imgPath"},
{NameRole, "imgName"},
};
}
ImageModel(
ImageProvider* provider,
const Config::SortConfigItems& sortConfig,
QSize thumbnailSize,
QObject* parent = nullptr);
~ImageModel();
bool isLoading() const { return m_isLoading; }
int processedCount() const { return m_processedCount.load(std::memory_order_relaxed); }
int totalCount() const { return m_watcher.progressMaximum(); }
void sortUpdate();
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
void loadAndProcess(const QStringList& paths);
Q_INVOKABLE void stop();
Q_INVOKABLE QVariant dataAt(int index, const QString& roleName) const;
signals:
void isLoadingChanged();
void progressChanged();
void totalCountChanged();
void imageSelected(const QString& path);
private slots:
void onProgressValueChanged(int value);
void onProcessingFinished();
private:
ImageProvider* m_provider;
const Config::SortConfigItems& m_sortConfig;
QSize m_thumbnailSize;
QList<ImageData*> m_data;
QFutureWatcher<ImageData*> m_watcher;
bool m_isLoading = false;
std::atomic<int> m_processedCount{0};
QTimer m_progressUpdateTimer;
static constexpr int s_progressUpdateInterval = 30;
static constexpr int s_isLoadingUpdateInterval = 50;
};
#endif // WALLREEL_IMAGEMODEL_HPP
+24
View File
@@ -0,0 +1,24 @@
#include "imageprovider.hpp"
QImage ImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) {
QMutexLocker locker(&m_mutex);
if (!m_images.contains(id)) {
return QImage();
}
ImageData* data = m_images[id];
if (size) {
*size = data->getImage().size();
}
return data->getImage();
}
void ImageProvider::insert(ImageData* data) {
QMutexLocker locker(&m_mutex);
m_images.insert(data->getId(), data);
}
void ImageProvider::clear() {
QMutexLocker locker(&m_mutex);
m_images.clear();
}
+27
View File
@@ -0,0 +1,27 @@
#ifndef WALLREEL_IMAGEPROVIDER_HPP
#define WALLREEL_IMAGEPROVIDER_HPP
#include <QHash>
#include <QMutex>
#include <QQuickImageProvider>
#include "imagedata.hpp"
class ImageProvider : public QQuickImageProvider {
Q_OBJECT
public:
ImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {}
QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;
void insert(ImageData* data);
void clear();
private:
QMutex m_mutex;
QHash<QString, ImageData*> m_images;
};
#endif // WALLREEL_IMAGEPROVIDER_HPP
+1 -9
View File
@@ -1,10 +1,4 @@
/* #include "logger.hpp"
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-07 01:12:37
* @LastEditTime: 2026-01-15 06:26:35
* @Description: Implementation of logger.
*/
#include "logger.h"
#include <unistd.h> #include <unistd.h>
@@ -38,8 +32,6 @@ static bool checkIsColored(FILE* stream) {
// Custom message handler // Custom message handler
static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
Q_UNUSED(context);
QMutexLocker locker(&s_logMutex); QMutexLocker locker(&s_logMutex);
QString levelTag; QString levelTag;
+3 -9
View File
@@ -1,11 +1,5 @@
/* #ifndef WALLREEL_LOGGER_HPP
* @Author: Uyanide pywang0608@foxmail.com #define WALLREEL_LOGGER_HPP
* @Date: 2025-08-05 10:43:31
* @LastEditTime: 2026-01-15 06:25:57
* @Description: A simple thread-safe logger.
*/
#ifndef GENERAL_LOGGER_H
#define GENERAL_LOGGER_H
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QString> #include <QString>
@@ -48,4 +42,4 @@ class Logger {
static void quiet(); static void quiet();
}; };
#endif // GENERAL_LOGGER_H #endif // WALLREEL_LOGGER_HPP
+16 -11
View File
@@ -1,12 +1,5 @@
/* #ifndef WALLREEL_MISC_HPP
* @Author: Uyanide pywang0608@foxmail.com #define WALLREEL_MISC_HPP
* @Date: 2025-11-30 20:59:57
* @LastEditTime: 2026-01-18 06:36:13
* @Description: THE utils header that every project needs :)
*/
#ifndef UTILS_H
#define UTILS_H
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
@@ -16,6 +9,8 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <utility> #include <utility>
#include "version.h"
/** /**
* @brief Defer execution of a callable until the end of the current scope. * @brief Defer execution of a callable until the end of the current scope.
* *
@@ -96,7 +91,7 @@ inline QString expandPath(const QString& path) {
* @param path * @param path
* @return QString * @return QString
*/ */
static QString splitNameFromPath(const QString& path) { inline QString splitNameFromPath(const QString& path) {
QFileInfo fileInfo(path); QFileInfo fileInfo(path);
return fileInfo.fileName(); return fileInfo.fileName();
} }
@@ -125,4 +120,14 @@ inline bool checkImageFile(const QString& filePath) {
return formats.contains(ext.toUtf8()); return formats.contains(ext.toUtf8());
} }
#endif // UTILS_H inline 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;
}
QDir().mkpath(configDir);
return configDir;
}
#endif // WALLREEL_MISC_HPP
-56
View File
@@ -1,56 +0,0 @@
<?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="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="ImagesCarouselScrollArea" name="scrollArea">
<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>380</width>
<height>280</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>
-57
View File
@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LoadingIndicator</class>
<widget class="QWidget" name="LoadingIndicator">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>795</width>
<height>653</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" alignment="Qt::AlignmentFlag::AlignHCenter">
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
<property name="format">
<string>%v/%m</string>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
-137
View File
@@ -1,137 +0,0 @@
<?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="mainLayout">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="topLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" 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>
<resources/>
<connections/>
</ui>
-110
View File
@@ -1,110 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:32:27
* @LastEditTime: 2026-02-07 07:26:30
* @Description: Image item widget for displaying an image.
*/
#include "image_item.h"
#include <qimage.h>
#include <QImageReader>
#include "logger.h"
using namespace GeneralLogger;
ImageData* ImageData::create(const QString& p, const int initWidth, const int initHeight) {
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));
delete data;
return nullptr;
}
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();
double scaleFactor = std::max(widthRatio, heightRatio);
QSize scaledSize = originalSize * scaleFactor;
reader.setScaledSize(scaledSize);
}
if (!reader.read(data->image)) {
warn(QString("Failed to load image from path: %1").arg(p));
delete data;
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;
*data->image = data->image->copy(x, y, targetSize.width(), targetSize.height());
}
return data;
}
ImageItem::ImageItem(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) {
assert(data != nullptr);
setScaledContents(true);
if (!data->isValid()) {
setText(":(");
setAlignment(Qt::AlignCenter);
} else {
setPixmap(QPixmap::fromImage(data->getImage()));
data->releaseImage();
}
setFixedSize(itemWidth, itemHeight);
}
ImageItem::~ImageItem() {
if (m_scaleAnimation) {
m_scaleAnimation->stop();
delete m_scaleAnimation;
m_scaleAnimation = nullptr;
}
delete m_data;
}
void ImageItem::setFocus(bool focus, bool animate) {
if (!animate) {
setFixedSize(focus ? m_itemFocusSize : m_itemSize);
return;
}
if (m_scaleAnimation) {
m_scaleAnimation->stop();
delete m_scaleAnimation;
m_scaleAnimation = nullptr;
}
m_scaleAnimation = new QPropertyAnimation(this, "size");
m_scaleAnimation->setDuration(ImageItem::s_animationDuration);
m_scaleAnimation->setStartValue(size());
m_scaleAnimation->setEndValue(focus ? m_itemFocusSize : m_itemSize);
m_scaleAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(m_scaleAnimation,
&QPropertyAnimation::valueChanged,
this,
[this](const QVariant& value) {
setFixedSize(value.toSize());
});
m_scaleAnimation->start();
}
-98
View File
@@ -1,98 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-11-30 20:31:15
* @LastEditTime: 2026-01-15 07:10:21
* @Description: Image item widget for displaying an image.
*/
#ifndef IMAGE_ITEM_H
#define IMAGE_ITEM_H
#include <QDateTime>
#include <QFileInfo>
#include <QImage>
#include <QLabel>
#include <QPropertyAnimation>
class ImageData;
class ImageItem;
/**
* @brief Data structure to hold image information
* and can be safely created and passed between threads.
*/
class ImageData {
QFileInfo file;
QImage* image;
public:
static ImageData* create(const QString& p, const int initWidth, const int initHeight);
~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; }
[[nodiscard]] bool isValid() const { return !image->isNull(); }
[[nodiscard]] const QFileInfo& getFileInfo() const { return file; }
private:
ImageData(const QString& path) : file(path), image() {}
};
/**
* @brief Image label that displays an image,
* which should always be created in the main thread.
*/
class ImageItem : public QLabel {
Q_OBJECT
public:
static constexpr int s_animationDuration = 300;
explicit ImageItem(ImageData* data,
const int itemWidth,
const int itemHeight,
const int itemFocusWidth,
const int itemFocusHeight,
QWidget* parent = nullptr);
~ImageItem() override;
[[nodiscard]] QString getFileFullPath() const { return m_data->getFileInfo().absoluteFilePath(); }
[[nodiscard]] QString getFileName() const { return m_data->getFileInfo().fileName(); }
[[nodiscard]] QDateTime getFileDate() const { return m_data->getFileInfo().lastModified(); }
[[nodiscard]] const QImage& getThumbnail() const { return m_data->getImage(); }
[[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:
void mousePressEvent(QMouseEvent* event) override {
emit clicked(getFileFullPath());
QLabel::mousePressEvent(event);
}
private:
const ImageData* m_data;
QSize m_itemSize;
QSize m_itemFocusSize;
QPropertyAnimation* m_scaleAnimation = nullptr;
signals:
void clicked(const QString& path);
};
#endif // IMAGE_ITEM_H
-443
View File
@@ -1,443 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2026-01-15 07:33:22
* @Description: Animated carousel widget for displaying and selecting images.
*/
#include "images_carousel.h"
#include <assert.h>
#include <pthread.h>
// #include <stdlib.h>
#include <QLabel>
#include <QMetaObject>
#include <QScrollArea>
#include <QScrollBar>
#include <QThreadPool>
#include <QVector>
#include <functional>
#include "image_item.h"
#include "logger.h"
#include "ui_images_carousel.h"
#include "utils.h"
using namespace GeneralLogger;
ImagesCarousel::ImagesCarousel(const Config::StyleConfigItems& styleConfig,
const Config::SortConfigItems& sortConfig,
QWidget* parent)
: QWidget(parent),
ui(new Ui::ImagesCarousel),
m_itemWidth(styleConfig.imageWidth),
m_itemHeight(static_cast<int>(m_itemWidth / styleConfig.aspectRatio)),
m_itemFocusWidth(styleConfig.imageFocusWidth),
m_itemFocusHeight(static_cast<int>(styleConfig.imageFocusWidth / styleConfig.aspectRatio)),
m_sortType(sortConfig.type),
m_sortReverse(sortConfig.reverse),
m_noLoadingScreen(styleConfig.noLoadingScreen) {
ui->setupUi(this);
m_scrollArea = dynamic_cast<ImagesCarouselScrollArea*>(ui->scrollArea);
m_imagesLayout = dynamic_cast<QHBoxLayout*>(ui->scrollAreaWidgetContents->layout());
qRegisterMetaType<ImageData*>("ImageData*");
// Remove border
ui->scrollArea->setFrameShape(QFrame::NoFrame);
connect(this,
&ImagesCarousel::loadingCompleted,
this,
&ImagesCarousel::_onImagesLoaded);
// Auto focus when scrolling
m_scrollDebounceTimer = new QTimer(this);
m_scrollDebounceTimer->setSingleShot(true);
m_scrollDebounceTimer->setInterval(s_debounceInterval);
connect(m_scrollDebounceTimer,
&QTimer::timeout,
this,
[this]() {
_onScrollBarValueChanged(m_pendingScrollValue);
});
connect(ui->scrollArea->horizontalScrollBar(),
&QScrollBar::valueChanged,
this,
[this](int value) {
m_pendingScrollValue = value;
if (m_suppressAutoFocus) {
return;
}
m_scrollDebounceTimer->start();
});
}
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);
} else if (m_imageInsertQueueTimer) {
m_imageInsertQueueTimer->stop();
m_imageInsertQueueTimer->deleteLater();
m_imageInsertQueueTimer = nullptr;
}
// No images loaded
if (!getLoadedImagesCount()) {
return;
}
// Focus the first image
if (m_currentIndex < 0) {
m_currentIndex = 0;
// Ensure the layout events are processed before focusing
QTimer::singleShot(0, this, [this]() {
focusCurrImage();
});
}
// exit(1); // for debug
}
ImagesCarousel::~ImagesCarousel() {
delete ui;
if (m_scrollAnimation) {
m_scrollAnimation->stop();
m_scrollAnimation->deleteLater();
}
}
void ImagesCarousel::appendImages(const QStringList& paths) {
if (paths.isEmpty()) {
warn("No images to add to display.");
emit loadingCompleted(0);
return;
}
{
QMutexLocker locker(&m_countMutex);
m_addedImagesCount += paths.size();
}
m_animationEnabled = false;
if (!m_noLoadingScreen) {
_enableUIUpdates(false);
} else if (m_imageInsertQueueTimer == nullptr) {
m_imageInsertQueueTimer = new QTimer(this);
m_imageInsertQueueTimer->setInterval(s_processBatchTimeout);
connect(m_imageInsertQueueTimer,
&QTimer::timeout,
this,
&ImagesCarousel::_processImageInsertQueue);
m_imageInsertQueueTimer->start();
}
emit loadingStarted(paths.size());
for (const QString& path : paths) {
ImageLoader* loader = new ImageLoader(path, this);
QThreadPool::globalInstance()->start(loader);
}
}
ImageLoader::ImageLoader(const QString& path, ImagesCarousel* carousel)
: m_path(path),
m_carousel(carousel),
m_initWidth(carousel->m_itemFocusWidth),
m_initHeight(carousel->m_itemFocusHeight) {
setAutoDelete(true);
}
void ImagesCarousel::_insertImageQueue(ImageData* data) {
if (!m_noLoadingScreen) {
_insertImage(data);
return;
}
{
QMutexLocker locker(&m_imageInsertQueueMutex);
m_imageInsertQueue.enqueue(data);
}
}
int ImagesCarousel::_insertImage(ImageData* data) {
// Increase loaded count regardless of success or failure
Defer defer([this]() {
emit imageLoaded(getLoadedImagesCount());
{
QMutexLocker countLocker(&m_countMutex);
if (++m_processedImagesCount >= m_addedImagesCount) {
{
QMutexLocker stopSignLocker(&m_stopSignMutex);
if (m_stopSign) {
// if all stopped
emit stopped();
}
}
emit loadingCompleted(m_processedImagesCount);
}
}
return;
});
if (!data) return -1;
auto item = new ImageItem(
data,
m_itemWidth,
m_itemHeight,
m_itemFocusWidth,
m_itemFocusHeight,
this);
static const QVector<std::function<bool(const ImageItem*, const ImageItem*)>> 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();
},
};
// insert into correct position based on sort type and direction
qint64 insertPos = getLoadedImagesCount();
if (m_sortType != Config::SortType::None) {
auto cmp = cmpFuncs[static_cast<int>(m_sortType)];
auto reverse = m_sortReverse;
int left = 0, right = getLoadedImagesCount();
while (left < right) {
int mid = left + (right - left) / 2;
if (reverse ? cmp(getImageItemAt(mid), item) : cmp(item, getImageItemAt(mid))) {
right = mid;
} else {
left = mid + 1;
}
}
insertPos = left;
}
connect(item,
&ImageItem::clicked,
this,
&ImagesCarousel::_onItemClicked);
m_imagesLayout->insertWidget(insertPos, item);
return insertPos;
}
void ImagesCarousel::_processImageInsertQueue() {
QVector<ImageData*> batch;
{
QMutexLocker locker(&m_imageInsertQueueMutex);
while (!m_imageInsertQueue.isEmpty() && batch.size() < s_processBatchSize) {
batch.append(m_imageInsertQueue.dequeue());
}
}
if (m_noLoadingScreen) _enableUIUpdates(false);
int currPos = m_currentIndex;
bool anyLoaded = false;
for (ImageData* data : batch) {
int pos = _insertImage(data);
// Keep the focusing index correct
if (pos >= 0 && pos <= currPos) {
currPos++;
} else if (pos >= 0) {
anyLoaded = true;
}
}
if (m_currentIndex >= 0) {
// Update focusing index if any
m_currentIndex = currPos;
if (m_currentIndex < 0 || m_currentIndex >= getLoadedImagesCount()) {
m_currentIndex = 0;
}
} else if (anyLoaded) {
// Focus the first image if none focused before
m_currentIndex = 0;
}
if (m_noLoadingScreen) _enableUIUpdates(true);
focusCurrImage();
}
void ImagesCarousel::_enableUIUpdates(bool enable) {
m_imagesLayout->setEnabled(enable);
if (enable) {
m_imagesLayout->activate();
}
ui->scrollAreaWidgetContents->setUpdatesEnabled(enable);
}
void ImageLoader::run() {
ImageData* data = nullptr;
Defer defer([this, &data]() {
if (m_carousel.isNull()) {
delete data;
return;
}
QMetaObject::invokeMethod(m_carousel,
"_insertImageQueue",
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;
}
data = ImageData::create(m_path, m_initWidth, m_initHeight);
}
void ImagesCarousel::focusNextImage() {
const auto count = getLoadedImagesCount();
// If no focus, focus the first image
if (m_currentIndex < 0) {
if (!count) return;
m_currentIndex = 0;
focusCurrImage();
return;
}
if (count <= 1) return;
unfocusCurrImage();
m_currentIndex++;
if (m_currentIndex >= count) {
m_currentIndex = 0;
}
focusCurrImage();
}
void ImagesCarousel::focusPrevImage() {
const auto count = getLoadedImagesCount();
// If no focus, focus the last image
if (m_currentIndex < 0) {
if (!count) return;
m_currentIndex = count - 1;
focusCurrImage();
return;
}
if (count <= 1) return;
unfocusCurrImage();
m_currentIndex--;
if (m_currentIndex < 0) {
m_currentIndex = count - 1;
}
focusCurrImage();
}
void ImagesCarousel::unfocusCurrImage() {
if (m_currentIndex < 0) return;
if (m_currentIndex >= getLoadedImagesCount()) {
warn(QString("Invalid index to unfocus: %1").arg(m_currentIndex));
return;
}
auto item = getImageItemAt(m_currentIndex);
if (item) item->setFocus(false, m_animationEnabled);
}
int ImagesCarousel::_focusingLeftOffset(int index) {
int spacing = ui->scrollAreaWidgetContents->layout()->spacing();
int centerOffset = (m_itemWidth + spacing) * index + m_itemFocusWidth / 2 - spacing;
return centerOffset - ui->scrollArea->width() / 2;
}
void ImagesCarousel::focusCurrImage() {
// If no focus, do nothing
if (m_currentIndex < 0) return;
if (m_currentIndex >= getLoadedImagesCount()) {
warn(QString("Invalid index to focus: %1").arg(m_currentIndex));
return;
}
auto item = getImageItemAt(m_currentIndex);
if (!item) {
warn(QString("Failed to get item at index: %1").arg(m_currentIndex));
return;
}
item->setFocus(true, m_animationEnabled);
emit imageFocused(item->getFileFullPath(),
m_currentIndex,
getLoadedImagesCount());
auto hScrollBar = ui->scrollArea->horizontalScrollBar();
int leftOffset = _focusingLeftOffset(m_currentIndex);
if (leftOffset < 0) {
leftOffset = 0;
}
if (!m_animationEnabled) {
hScrollBar->setValue(leftOffset);
return;
}
if (m_scrollAnimation) {
m_scrollAnimation->stop();
m_scrollAnimation->deleteLater();
m_scrollAnimation = nullptr;
}
m_scrollAnimation = new QPropertyAnimation(hScrollBar, "value");
m_scrollAnimation->setDuration(s_animationDuration);
m_scrollAnimation->setStartValue(hScrollBar->value());
m_scrollAnimation->setEndValue(leftOffset);
m_scrollAnimation->setEasingCurve(QEasingCurve::OutCubic);
// Suppress auto focus during animation
connect(m_scrollAnimation,
&QPropertyAnimation::finished,
this,
[this]() {
m_suppressAutoFocus = false;
m_scrollArea->setBlockInput(false);
});
m_suppressAutoFocus = true;
m_scrollArea->setBlockInput(true);
m_scrollAnimation->start();
}
void ImagesCarousel::_onScrollBarValueChanged(int value) {
// Stop the animation if it is running
if (m_scrollAnimation && m_scrollAnimation->state() == QPropertyAnimation::Running) {
m_scrollAnimation->stop();
m_scrollAnimation->deleteLater();
m_scrollAnimation = nullptr;
}
int centerOffset = (value + m_itemFocusWidth / 2);
int itemOffset = m_itemWidth + ui->scrollAreaWidgetContents->layout()->spacing();
int index = centerOffset / itemOffset;
if (index < 0 || index >= getLoadedImagesCount()) {
return; // Out of bounds
}
if (index == m_currentIndex) {
return; // Already focused
}
unfocusCurrImage();
m_currentIndex = index;
focusCurrImage();
}
void ImagesCarousel::_onItemClicked(const QString& path) {
// if (m_suppressAutoFocus) return;
unfocusCurrImage();
// Most likely the clicked item is near the current index
const auto count = getLoadedImagesCount();
for (int i = m_currentIndex, j = m_currentIndex + 1;
i >= 0 || j < count;
--i, ++j) {
if (i >= 0 && getImageItemAt(i)->getFileFullPath() == path) {
m_currentIndex = i;
break;
}
if (j < count && getImageItemAt(j)->getFileFullPath() == path) {
m_currentIndex = j;
break;
}
}
focusCurrImage();
}
void ImagesCarousel::onStop() {
QMutexLocker locker(&m_stopSignMutex);
m_stopSign = true;
}
-286
View File
@@ -1,286 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 01:22:53
* @LastEditTime: 2026-01-18 06:39:44
* @Description: Animated carousel widget for displaying and selecting images.
*/
#ifndef IMAGES_CAROUSEL_H
#define IMAGES_CAROUSEL_H
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QMutex>
#include <QPointer>
#include <QPropertyAnimation>
#include <QQueue>
#include <QRunnable>
#include <QScrollArea>
#include <QTimer>
#include "config.h"
#include "image_item.h"
/* Design Notes:
Two different image loading strategies:
- With loading screen: load all images directly
1. appendImages called -> increace m_addedImagesCount & disable UI updates &
spawn and start 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 and start all
ImageLoader threads and a timer m_imageInsertQueueTimer & disable animations
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 regardless of whether loading screen is used or not:
- 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;
/**
* @brief Worker class for loading images in a separate thread.
*/
class ImageLoader : public QRunnable {
public:
ImageLoader(const QString& path, ImagesCarousel* carousel);
void run() override; // friend to ImagesCarousel
private:
QString m_path;
QPointer<ImagesCarousel> m_carousel;
const int m_initWidth;
const int m_initHeight;
};
namespace Ui {
class ImagesCarousel;
}
class ImagesCarousel : public QWidget {
Q_OBJECT
friend void ImageLoader::run();
public:
explicit ImagesCarousel(const Config::StyleConfigItems& styleConfig,
const Config::SortConfigItems& sortConfig,
QWidget* parent = nullptr);
~ImagesCarousel();
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);
if (item) {
return item->getFileFullPath();
}
}
return "";
}
/**
* @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;
}
return dynamic_cast<ImageItem*>(
m_imagesLayout
->itemAt(index)
->widget());
}
/**
* @brief Get count of added images
*
* @return qsizetype
*/
[[nodiscard]] qsizetype getAddedImagesCount() {
QMutexLocker locker(&m_countMutex);
return m_addedImagesCount;
}
// config items
const int m_itemWidth;
const int m_itemHeight;
const int m_itemFocusWidth;
const int m_itemFocusHeight;
const Config::SortType m_sortType;
const bool m_sortReverse;
const bool m_noLoadingScreen;
public slots:
void focusNextImage();
void focusPrevImage();
void focusCurrImage();
void unfocusCurrImage();
void onStop();
private slots:
void _onScrollBarValueChanged(int value);
void _onItemClicked(const QString& path);
void _onImagesLoaded(); // Called when loading is completed or stopped
void _processImageInsertQueue();
public:
void appendImages(const QStringList& paths);
private:
int _insertImage(ImageData* item);
Q_INVOKABLE void _insertImageQueue(ImageData* item);
void _enableUIUpdates(bool enable);
int _focusingLeftOffset(int index);
private:
// UI elements
Ui::ImagesCarousel* ui;
QHBoxLayout* m_imagesLayout = nullptr;
ImagesCarouselScrollArea* m_scrollArea = nullptr;
// Items and counters
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_processedImagesCount and m_addedImagesCount
int m_currentIndex = -1; // initially no focus
// Threading
QQueue<ImageData*> m_imageInsertQueue;
QMutex m_imageInsertQueueMutex;
QTimer* m_imageInsertQueueTimer = nullptr;
// Animations
QPropertyAnimation* m_scrollAnimation = nullptr;
bool m_animationEnabled = true;
// Auto focusing
bool m_suppressAutoFocus = false;
int m_pendingScrollValue = 0;
QTimer* m_scrollDebounceTimer = nullptr;
// Loading stopped by user
QMutex m_stopSignMutex;
bool m_stopSign = false;
signals:
void imageFocused(const QString& path, const int index, const int count);
void loadingStarted(const qsizetype amount);
void loadingCompleted(const qsizetype amount);
void imageLoaded(const qsizetype count);
void stopped();
};
class ImagesCarouselScrollArea : public QScrollArea {
Q_OBJECT
public:
explicit ImagesCarouselScrollArea(QWidget* parent = nullptr)
: QScrollArea(parent) {
// setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// setWidgetResizable(true);
}
void setBlockInput(bool block) { m_blockInput = block; }
protected:
void keyPressEvent(QKeyEvent* event) override {
if (m_blockInput) {
event->ignore();
return;
}
if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right) {
event->ignore();
} else {
QScrollArea::keyPressEvent(event);
}
}
void wheelEvent(QWheelEvent* event) override {
if (m_blockInput) {
event->ignore();
return;
}
if (event->angleDelta().y() != 0) {
event->ignore();
} else {
QScrollArea::wheelEvent(event);
}
}
private:
bool m_blockInput = false;
};
#endif // IMAGES_CAROUSEL_H
-18
View File
@@ -1,18 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-07 00:32:25
* @LastEditTime: 2026-01-15 05:23:55
* @Description: LoadingIndicator implementation.
*/
#include "loading_indicator.h"
LoadingIndicator::LoadingIndicator(int barMinimumWidth, QWidget* parent)
: QWidget(parent),
ui(new Ui::LoadingIndicator) {
ui->setupUi(this);
ui->progressBar->setMinimumWidth(barMinimumWidth);
}
LoadingIndicator::~LoadingIndicator() {
delete ui;
}
-33
View File
@@ -1,33 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-07 00:32:25
* @LastEditTime: 2026-01-15 05:24:14
* @Description: A loading indicator widget with a progress bar.
*/
#ifndef LOADING_INDICATOR_H
#define LOADING_INDICATOR_H
#include <QWidget>
#include "ui_loading_indicator.h"
namespace Ui {
class LoadingIndicator;
}
class LoadingIndicator : public QWidget {
Q_OBJECT
public:
explicit LoadingIndicator(int barMinimumWidth = 500, QWidget* parent = nullptr);
~LoadingIndicator();
void setMaximum(int max) { ui->progressBar->setMaximum(max); }
void setValue(int value) { ui->progressBar->setValue(value); }
private:
Ui::LoadingIndicator* ui;
};
#endif // LOADING_INDICATOR_H
+46 -24
View File
@@ -1,19 +1,17 @@
/* #include <qtypes.h>
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2026-01-15 14:11:06
* @Description: Argument parser and entry point.
*/
#include <QApplication> #include <QApplication>
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QDir> #include <QDir>
#include <QQmlApplicationEngine>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTextStream> #include <QTextStream>
#include "config.h" #include "core/configmgr.hpp"
#include "logger.h" #include "core/imagemodel.hpp"
#include "main_window.h" #include "core/imageprovider.hpp"
#include "utils.h" #include "core/utils/logger.hpp"
#include "core/utils/misc.hpp"
#include "version.h" #include "version.h"
/** /**
@@ -108,7 +106,7 @@ static class AppOptions {
Logger::quiet(); Logger::quiet();
} else { } else {
// Default to INFO level // Default to INFO level
Logger::setLogLevel(QtInfoMsg); Logger::setLogLevel(QtDebugMsg);
} }
for (const QString& dir : parser.values(appendDirOption)) { for (const QString& dir : parser.values(appendDirOption)) {
@@ -135,16 +133,6 @@ static class AppOptions {
} s_options; } 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;
}
QDir().mkpath(configDir);
return configDir;
}
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
QApplication a(argc, argv); QApplication a(argc, argv);
a.setApplicationName(APP_NAME); a.setApplicationName(APP_NAME);
@@ -157,10 +145,44 @@ int main(int argc, char* argv[]) {
return s_options.errorText.isEmpty() ? 0 : 1; return s_options.errorText.isEmpty() ? 0 : 1;
} }
Config config(getConfigDir(), s_options.appendDirs, s_options.configPath, &a); Config config(
::getConfigDir(),
s_options.appendDirs,
s_options.configPath,
&a);
QQmlApplicationEngine engine;
MainWindow w(config); ImageProvider* imageProvider = new ImageProvider();
w.show(); engine.addImageProvider(QLatin1String("processed"), imageProvider);
ImageModel imageModel(
imageProvider,
config.getSortConfig(),
config.getFocusImageSize(),
&a);
qmlRegisterSingletonInstance(
COREMODULE_URI,
MODULE_VERSION_MAJOR,
MODULE_VERSION_MINOR,
"Config",
&config);
qmlRegisterSingletonInstance(
COREMODULE_URI,
MODULE_VERSION_MAJOR,
MODULE_VERSION_MINOR,
"ImageModel",
&imageModel);
QObject::connect(
&engine,
&QQmlApplicationEngine::objectCreationFailed,
&a,
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.loadFromModule(UIMODULE_URI, "Main");
imageModel.loadAndProcess(config.getWallpapers());
return a.exec(); return a.exec();
} }
-285
View File
@@ -1,285 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2026-01-15 07:25:41
* @Description: MainWindow implementation.
*/
#include "main_window.h"
#include <QDir>
#include <QKeyEvent>
#include <QProcess>
#include <QPushButton>
#include <functional>
#include "./ui_main_window.h"
#include "images_carousel.h"
#include "logger.h"
#include "utils.h"
using namespace GeneralLogger;
MainWindow::MainWindow(const Config& config, QWidget* parent)
: QMainWindow(parent), ui(new Ui::MainWindow), m_config(config) {
ui->setupUi(this);
_setupUI();
}
MainWindow::~MainWindow() {
delete ui;
}
void MainWindow::_setupUI() {
// change window title
setWindowTitle("Wallpaper Carousel");
// create images carousel
m_carousel = new ImagesCarousel(
m_config.getStyleConfig(),
m_config.getSortConfig(),
this);
ui->mainLayout->insertWidget(2, m_carousel);
connect(m_carousel,
&ImagesCarousel::imageFocused,
this,
&MainWindow::_onImageFocused);
connect(this, &MainWindow::stop, m_carousel, &ImagesCarousel::onStop);
m_carouselIndex = ui->stackedWidget->addWidget(m_carousel);
// create loading indicator
int barWidth = m_config.getStyleConfig().windowWidth * 0.7;
m_loadingIndicator = new LoadingIndicator(barWidth, this);
connect(m_carousel,
&ImagesCarousel::loadingStarted,
this,
&MainWindow::_onLoadingStarted);
connect(m_carousel,
&ImagesCarousel::loadingCompleted,
this,
&MainWindow::_onLoadingCompleted);
connect(m_carousel,
&ImagesCarousel::imageLoaded,
m_loadingIndicator,
&LoadingIndicator::setValue);
m_loadingIndicatorIndex = ui->stackedWidget->addWidget(m_loadingIndicator);
// set window size
setMinimumSize(m_config.getStyleConfig().windowWidth, m_config.getStyleConfig().windowHeight);
setMaximumSize(m_config.getStyleConfig().windowWidth, m_config.getStyleConfig().windowHeight);
connect(ui->confirmButton,
&QPushButton::clicked,
this,
&MainWindow::_onConfirmPressed);
connect(ui->cancelButton,
&QPushButton::clicked,
this,
&MainWindow::_onCancelPressed);
ui->confirmButton->setFocusPolicy(Qt::NoFocus);
ui->cancelButton->setFocusPolicy(Qt::NoFocus);
m_carousel->appendImages(m_config.getWallpapers());
}
void MainWindow::keyPressEvent(QKeyEvent* event) {
// Same effects as clicking the confirm/cancel buttons
if (event->key() == Qt::Key_Escape) {
_onCancelPressed();
return;
}
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
_onConfirmPressed();
return;
}
switch (m_state) {
case Init:
MainWindow::keyPressEvent(event);
break;
case Loading:
if (m_config.getStyleConfig().noLoadingScreen) {
switch (event->key()) {
case Qt::Key_Space:
case Qt::Key_Tab:
case Qt::Key_Right:
m_carousel->focusNextImage();
break;
case Qt::Key_Left:
m_carousel->focusPrevImage();
break;
default:
QMainWindow::keyPressEvent(event);
}
} else {
event->ignore();
}
break;
case Ready:
switch (event->key()) {
case Qt::Key_Space:
case Qt::Key_Tab:
case Qt::Key_Right:
m_carousel->focusNextImage();
break;
case Qt::Key_Left:
m_carousel->focusPrevImage();
break;
default:
QMainWindow::keyPressEvent(event);
}
break;
default:
event->ignore();
break;
}
}
// 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;
}
debug("Stopping loading...");
connect(
m_carousel,
&ImagesCarousel::stopped,
this,
[this, onStopped]() {
if (onStopped) {
onStopped();
}
});
m_state = Stopping;
emit stop();
}
void MainWindow::_onCancelPressed() {
switch (m_state) {
case Loading:
// case loading screen is disabled, quit the app
if (m_config.getStyleConfig().noLoadingScreen) {
debug("Stopping loading...");
_stopLoadingAndQuit([this]() {
debug("Quitting app...");
close();
});
}
// otherwise, stop loading and display the loaded images
else {
debug("Stopping loading and displaying loaded images...");
_stopLoadingAndQuit([this]() {
debug("Loading stopped.");
// and do nothing
});
}
break;
case Ready:
debug("Quitting app...");
close();
break;
default:
break;
}
}
void MainWindow::_onConfirmPressed() {
switch (m_state) {
case Loading:
// case loading screen is disabled, confirm the selection
if (m_config.getStyleConfig().noLoadingScreen) {
debug("Stopping loading and confirming selection...");
// 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:
debug("Confirming selection...");
onConfirm();
break;
default:
break;
}
}
void MainWindow::wheelEvent(QWheelEvent* event) {
if (m_state != Ready && m_config.getStyleConfig().noLoadingScreen) {
event->ignore();
return;
}
// angleDelta().x() is handled by QScrollArea
if (event->angleDelta().y() > 0) {
m_carousel->focusPrevImage();
} else if (event->angleDelta().y() < 0) {
m_carousel->focusNextImage();
} else {
QMainWindow::wheelEvent(event);
}
}
void MainWindow::closeEvent(QCloseEvent* event) {
if (m_state == Loading) {
event->ignore();
_stopLoadingAndQuit([this]() {
debug("Quitting app...");
close();
});
} else {
event->accept();
}
}
void MainWindow::onConfirm() {
close();
_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;
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))) {
critical(QString("Failed to execute command: %1").arg(cmd));
return;
}
}
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));
}
void MainWindow::_onLoadingStarted(const qsizetype amount) {
m_state = Loading;
if (m_config.getStyleConfig().noLoadingScreen) {
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;
}
-73
View File
@@ -1,73 +0,0 @@
/*
* @Author: Uyanide pywang0608@foxmail.com
* @Date: 2025-08-05 00:37:58
* @LastEditTime: 2026-01-15 06:16:07
* @Description: MainWindow implementation.
*/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "config.h"
#include "images_carousel.h"
#include "loading_indicator.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(const Config& config, QWidget* parent = nullptr);
~MainWindow();
public slots:
void onConfirm();
protected:
void keyPressEvent(QKeyEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
void closeEvent(QCloseEvent* event) override;
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);
void _onLoadingStarted(const qsizetype amount);
void _onLoadingCompleted(const qsizetype amount);
void _onCancelPressed();
void _onConfirmPressed();
private:
enum _State {
Init,
Loading,
Stopping,
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;
int m_carouselIndex, m_loadingIndicatorIndex;
const Config& m_config;
signals:
void stop();
};
#endif // MAINWINDOW_H
+9
View File
@@ -0,0 +1,9 @@
qt_add_qml_module(${UILIB_NAME}
URI ${UIMODULE_URI}
VERSION ${MODULE_VERSION_MAJOR}.${MODULE_VERSION_MINOR}
QML_FILES
Main.qml
Pages/LoadingScreen.qml
Pages/CarouselScreen.qml
Modules/Carousel.qml
)
+33
View File
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Controls
import WallReel.Core
import WallReel.UI.Pages
ApplicationWindow {
width: Config.windowWidth
height: Config.windowHeight
minimumWidth: width
maximumWidth: width
minimumHeight: height
maximumHeight: height
visible: true
title: qsTr("Hello World")
LoadingScreen {
visible: ImageModel.isLoading
anchors.fill: parent
currentValue: ImageModel.processedCount
totalValue: ImageModel.totalCount
}
Loader {
anchors.fill: parent
active: !ImageModel.isLoading
sourceComponent: CarouselScreen {
visible: !ImageModel.isLoading
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import QtQuick
Item {
id: root
required property var model
property int currentIndex: 0
property int itemWidth: 300
property int itemHeight: 400
property int focusedItemWidth: 450
property int focusedItemHeight: 600
property int spacing: 0
property int animDuration: 200
property int count: view.count
ListView {
id: view
model: root.model
anchors.fill: parent
orientation: ListView.Horizontal
spacing: root.spacing
highlightRangeMode: ListView.StrictlyEnforceRange
snapMode: ListView.SnapToItem
highlightMoveDuration: root.animDuration
preferredHighlightBegin: (width - root.focusedItemWidth) / 2
preferredHighlightEnd: (width + root.focusedItemWidth) / 2
clip: true
cacheBuffer: root.itemWidth * 3
onCurrentIndexChanged: {
if (root.currentIndex !== view.currentIndex)
root.currentIndex = view.currentIndex;
}
Component.onCompleted: {
view.currentIndex = root.currentIndex;
view.forceActiveFocus();
}
Connections {
function onCurrentIndexChanged() {
if (view.currentIndex !== root.currentIndex)
view.currentIndex = root.currentIndex;
}
target: root
}
delegate: Item {
id: delegateItem
property bool isFocused: ListView.isCurrentItem
width: isFocused ? root.focusedItemWidth : root.itemWidth
height: view.height
z: isFocused ? 100 : 1
Rectangle {
anchors.centerIn: parent
width: parent.width
height: delegateItem.isFocused ? root.focusedItemHeight : root.itemHeight
color: "transparent"
Image {
id: img
anchors.fill: parent
source: "image://processed/" + model.imgId
fillMode: Image.PreserveAspectFit
asynchronous: true
cache: true
}
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = index;
view.forceActiveFocus();
}
}
Behavior on height {
NumberAnimation {
duration: root.animDuration
easing.type: Easing.OutCubic
}
}
}
Behavior on width {
NumberAnimation {
duration: root.animDuration
easing.type: Easing.OutCubic
}
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import WallReel.Core
import WallReel.UI.Modules
Item {
id: root
Keys.onPressed: (e) => {
if (e.key === Qt.Key_Left) {
if (carousel.currentIndex > 0)
carousel.currentIndex--;
} else if (e.key === Qt.Key_Right) {
if (carousel.currentIndex < carousel.count - 1)
carousel.currentIndex++;
} else if (e.key === Qt.Key_Escape)
Qt.quit();
else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)
ImageModel.confirm(carousel.currentIndex);
else
e.accepted = false;
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 20
Label {
text: (ImageModel.dataAt(carousel.currentIndex, "imgName") ?? "") + " (" + (carousel.currentIndex + 1) + "/" + carousel.count + ")"
font.pixelSize: 12
Layout.alignment: Qt.AlignHCenter
}
Carousel {
id: carousel
Layout.fillWidth: true
Layout.fillHeight: true
model: ImageModel
itemWidth: Config.imageWidth
itemHeight: Config.imageWidth / Config.aspectRatio
focusedItemWidth: Config.imageFocusWidth
focusedItemHeight: Config.imageFocusWidth / Config.aspectRatio
MouseArea {
anchors.fill: parent
onWheel: (e) => {
if (e.angleDelta.y > 0) {
if (carousel.currentIndex > 0)
carousel.currentIndex--;
} else if (e.angleDelta.y < 0) {
if (carousel.currentIndex < carousel.count - 1)
carousel.currentIndex++;
}
}
onPressed: (e) => {
carousel.forceActiveFocus();
e.accepted = false;
}
onPositionChanged: (e) => {
e.accepted = false;
}
onReleased: (e) => {
e.accepted = false;
}
}
}
Slider {
id: progressBar
Layout.fillWidth: true
from: 0
to: carousel.count - 1
value: carousel.currentIndex
onMoved: {
if (carousel.currentIndex !== value)
carousel.currentIndex = value;
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Controls
Item {
property int currentValue: 0
property int totalValue: 100
Label {
anchors.bottom: loadingBar.top
anchors.horizontalCenter: loadingBar.horizontalCenter
anchors.bottomMargin: 0
text: currentValue + "/" + totalValue
font.pixelSize: 12
}
ProgressBar {
id: loadingBar
anchors.centerIn: parent
width: parent.width * 0.8
value: totalValue > 0 ? currentValue / totalValue : 0
}
}
+11
View File
@@ -1,2 +1,13 @@
#ifndef VERSION_H
#define VERSION_H
// clang-format off
#define APP_VERSION "@PROJECT_VERSION@" #define APP_VERSION "@PROJECT_VERSION@"
#define APP_NAME "@EXECUTABLE_NAME@" #define APP_NAME "@EXECUTABLE_NAME@"
#define COREMODULE_URI "@COREMODULE_URI@"
#define UIMODULE_URI "@UIMODULE_URI@"
#define MODULE_VERSION_MAJOR @MODULE_VERSION_MAJOR@
#define MODULE_VERSION_MINOR @MODULE_VERSION_MINOR@
// clang-format on
#endif