init
This commit is contained in:
@@ -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
@@ -1,8 +1,83 @@
|
||||
.cache
|
||||
.vscode
|
||||
.qtcreator
|
||||
build-dbg
|
||||
build
|
||||
*.log
|
||||
# This file is used to ignore files which are generated
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
*.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
@@ -1,18 +1,15 @@
|
||||
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_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
@@ -20,72 +17,50 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
|
||||
|
||||
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()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
add_definitions(-DSUPPRESS_QRC_LOG)
|
||||
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")
|
||||
# target_compile_definitions(wallpaper_chooser PRIVATE
|
||||
# GENERAL_LOGGER_DISABLED
|
||||
# )
|
||||
# endif()
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
|
||||
qt_policy(SET QTP0004 NEW)
|
||||
|
||||
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.
|
||||
# If you are developing for iOS or macOS you should consider setting an
|
||||
# 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
|
||||
${BUNDLE_ID_OPTION}
|
||||
|
||||
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.wallpaper-carousel
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||
MACOSX_BUNDLE 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)
|
||||
install(TARGETS ${EXECUTABLE_NAME}
|
||||
BUNDLE DESTINATION .
|
||||
@@ -96,7 +71,3 @@ install(TARGETS ${EXECUTABLE_NAME}
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/app/${EXECUTABLE_NAME}.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
||||
)
|
||||
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
qt_finalize_executable(${EXECUTABLE_NAME})
|
||||
endif()
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -1,10 +1,4 @@
|
||||
/*
|
||||
* @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 "configmgr.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@@ -14,8 +8,8 @@
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "logger.h"
|
||||
#include "utils.h"
|
||||
#include "utils/logger.hpp"
|
||||
#include "utils/misc.hpp"
|
||||
using namespace GeneralLogger;
|
||||
|
||||
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));
|
||||
}
|
||||
}},
|
||||
{"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) {
|
||||
if (val.isString()) {
|
||||
QString type = val.toString().toLower();
|
||||
@@ -188,16 +176,16 @@ void Config::_loadWallpapers() {
|
||||
QSet<QString> paths;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (checkDir(dirPath)) {
|
||||
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);
|
||||
paths.insert(expandPath(filePath));
|
||||
}
|
||||
@@ -207,7 +195,7 @@ void Config::_loadWallpapers() {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @Date: 2025-08-05 01:34:52
|
||||
* @LastEditTime: 2026-01-15 07:18:46
|
||||
* @Description: Configuration manager.
|
||||
*/
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||
#define WALLREEL_CONFIGMGR_HPP
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
@@ -26,7 +22,6 @@
|
||||
// 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
|
||||
@@ -34,6 +29,12 @@
|
||||
class Config : public QObject {
|
||||
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:
|
||||
enum class SortType : int {
|
||||
None = 0, // "none"
|
||||
@@ -58,7 +59,6 @@ class Config : public QObject {
|
||||
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 {
|
||||
@@ -74,17 +74,33 @@ class Config : public QObject {
|
||||
|
||||
~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;
|
||||
const QString m_configDir;
|
||||
@@ -102,4 +118,4 @@ class Config : public QObject {
|
||||
QStringList m_wallpapers;
|
||||
};
|
||||
|
||||
#endif // CONFIG_H
|
||||
#endif // WALLREEL_CONFIGMGR_HPP
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,10 +1,4 @@
|
||||
/*
|
||||
* @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 "logger.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -38,8 +32,6 @@ static bool checkIsColored(FILE* stream) {
|
||||
|
||||
// Custom message handler
|
||||
static void messageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) {
|
||||
Q_UNUSED(context);
|
||||
|
||||
QMutexLocker locker(&s_logMutex);
|
||||
|
||||
QString levelTag;
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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
|
||||
#ifndef WALLREEL_LOGGER_HPP
|
||||
#define WALLREEL_LOGGER_HPP
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QString>
|
||||
@@ -48,4 +42,4 @@ class Logger {
|
||||
static void quiet();
|
||||
};
|
||||
|
||||
#endif // GENERAL_LOGGER_H
|
||||
#endif // WALLREEL_LOGGER_HPP
|
||||
@@ -1,12 +1,5 @@
|
||||
/*
|
||||
* @Author: Uyanide pywang0608@foxmail.com
|
||||
* @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
|
||||
#ifndef WALLREEL_MISC_HPP
|
||||
#define WALLREEL_MISC_HPP
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
@@ -16,6 +9,8 @@
|
||||
#include <QStandardPaths>
|
||||
#include <utility>
|
||||
|
||||
#include "version.h"
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @return QString
|
||||
*/
|
||||
static QString splitNameFromPath(const QString& path) {
|
||||
inline QString splitNameFromPath(const QString& path) {
|
||||
QFileInfo fileInfo(path);
|
||||
return fileInfo.fileName();
|
||||
}
|
||||
@@ -125,4 +120,14 @@ inline bool checkImageFile(const QString& filePath) {
|
||||
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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -1,19 +1,17 @@
|
||||
/*
|
||||
* @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 <qtypes.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDir>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "config.h"
|
||||
#include "logger.h"
|
||||
#include "main_window.h"
|
||||
#include "utils.h"
|
||||
#include "core/configmgr.hpp"
|
||||
#include "core/imagemodel.hpp"
|
||||
#include "core/imageprovider.hpp"
|
||||
#include "core/utils/logger.hpp"
|
||||
#include "core/utils/misc.hpp"
|
||||
#include "version.h"
|
||||
|
||||
/**
|
||||
@@ -108,7 +106,7 @@ static class AppOptions {
|
||||
Logger::quiet();
|
||||
} else {
|
||||
// Default to INFO level
|
||||
Logger::setLogLevel(QtInfoMsg);
|
||||
Logger::setLogLevel(QtDebugMsg);
|
||||
}
|
||||
|
||||
for (const QString& dir : parser.values(appendDirOption)) {
|
||||
@@ -135,16 +133,6 @@ 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;
|
||||
}
|
||||
QDir().mkpath(configDir);
|
||||
return configDir;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication a(argc, argv);
|
||||
a.setApplicationName(APP_NAME);
|
||||
@@ -157,10 +145,44 @@ int main(int argc, char* argv[]) {
|
||||
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);
|
||||
w.show();
|
||||
ImageProvider* imageProvider = new ImageProvider();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,13 @@
|
||||
#ifndef VERSION_H
|
||||
#define VERSION_H
|
||||
|
||||
// clang-format off
|
||||
#define APP_VERSION "@PROJECT_VERSION@"
|
||||
#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
|
||||
|
||||
Reference in New Issue
Block a user