diff --git a/.appveyor.yml b/.appveyor.yml index 1c9a62130..61402da90 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,11 +5,13 @@ environment: DEPLOY: 0 MYSQL_VERSION: mysql-5.7.14-win32 APNG_PLUGIN: apng-1.1.05.10.1 + QSCINTILLA_VERSION: QScintilla_gpl-2.10.8 matrix: # MSVC x86 - PLATFORM: amd64_x86 QTDIR: C:\Qt\5.10.1\msvc2015 + OPENSSLDIR: C:\OpenSSL-Win32 MAKE: nmake MAKEFILES: NMake Makefiles DEPLOY: 1 @@ -17,12 +19,15 @@ environment: # MSVC x64 - PLATFORM: amd64 QTDIR: C:\Qt\5.10.1\msvc2015_64 + OPENSSLDIR: C:\OpenSSL-Win64 MAKE: nmake MAKEFILES: NMake Makefiles + DEPLOY: 1 # MinGW - PLATFORM: mingw QTDIR: C:\Qt\5.10.1\mingw53_32 + OPENSSLDIR: C:\OpenSSL-Win32 MAKE: mingw32-make MAKEFILES: MinGW Makefiles @@ -30,6 +35,9 @@ cache: - release\sites\node_modules -> release\sites\package.json - mysql-5.7.14-win32 -> .appveyor.yml - apng-1.1.05.10.1 -> .appveyor.yml + - QScintilla_gpl-2.10.8 -> .appveyor.yml + - depot_tools -> .appveyor.yml + - breakpad -> .appveyor.yml init: - git config --global core.autocrlf input @@ -38,33 +46,87 @@ init: - set PATH=%QTDIR%\bin;%PATH% - if not %PLATFORM%==mingw call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %PLATFORM% - set PATH=%PATH%;"C:\\Program Files (x86)\\Inno Setup 5" + - if "%APPVEYOR_REPO_TAG%"=="true" (set "GRABBER_IS_NIGHTLY=0") else (set "GRABBER_IS_NIGHTLY=1") + - if "%APPVEYOR_REPO_TAG%"=="true" (set "GRABBER_VERSION=%APPVEYOR_REPO_TAG_NAME%") else (set "GRABBER_VERSION=nightly") + - if "%PLATFORM%"=="X86" (set "PLATFORM_NAME=x86") else (set "PLATFORM_NAME=x64") + +install: + - git submodule update --init --recursive build_script: + # Download Google tools and put them in the PATH (required for Google Breakpad) + - if not exist "depot_tools" appveyor DownloadFile https://storage.googleapis.com/chrome-infra/depot_tools.zip + - if not exist "depot_tools" 7z x "depot_tools.zip" -odepot_tools -y + - depot_tools\update_depot_tools + - set PATH=%APPVEYOR_BUILD_FOLDER%\depot_tools;%PATH% + + # Build Google Breakpad + - set "BREAKPAD_BUILD=0" + - if %DEPLOY%==1 if not exist "breakpad" set "BREAKPAD_BUILD=1" + - if %BREAKPAD_BUILD%==1 mkdir breakpad + - if %BREAKPAD_BUILD%==1 cd breakpad + - if %BREAKPAD_BUILD%==1 gclient config https://github.com/google/breakpad --unmanaged --name=src + - if %BREAKPAD_BUILD%==1 gclient sync + - if %BREAKPAD_BUILD%==1 cd src\src\client\windows + - if %BREAKPAD_BUILD%==1 sed -i -e "s/<\/RuntimeLibrary>/DLL<\/RuntimeLibrary>/g;" *.vcxproj # false<\/TreatWChar_tAsBuiltInType> + - if %BREAKPAD_BUILD%==1 sed -i -e "s/<\/RuntimeLibrary>/DLL<\/RuntimeLibrary>/g;" */*.vcxproj + - if %BREAKPAD_BUILD%==1 sed -i -e "s/<\/RuntimeLibrary>/DLL<\/RuntimeLibrary>/g;" */*/*.vcxproj + - if %BREAKPAD_BUILD%==1 if "%PLATFORM%"=="X86" (set "BREAKPAD_PLATFORM=Win32") else (set "BREAKPAD_PLATFORM=x64") + - if %BREAKPAD_BUILD%==1 msbuild breakpad_client.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" /m /verbosity:normal /p:Configuration=Release /p:Platform="%BREAKPAD_PLATFORM%" + - if %BREAKPAD_BUILD%==1 cd "%APPVEYOR_BUILD_FOLDER%" + + # Build QScintilla + - set "QSCINTILLA_BUILD=0" + - if %DEPLOY%==1 if not exist %QSCINTILLA_VERSION% set "QSCINTILLA_BUILD=1" + - if %QSCINTILLA_BUILD%==1 curl -L "https://sourceforge.net/projects/pyqt/files/QScintilla2/QScintilla-2.10.8/%QSCINTILLA_VERSION%.zip" -o "%QSCINTILLA_VERSION%.zip" + - if %QSCINTILLA_BUILD%==1 7z x "%QSCINTILLA_VERSION%.zip" -y + - if %DEPLOY%==1 call "%QTDIR%/bin/qtenv2.bat" + - if %DEPLOY%==1 cd "%APPVEYOR_BUILD_FOLDER%/%QSCINTILLA_VERSION%/Qt4Qt5" + - if %QSCINTILLA_BUILD%==1 qmake qscintilla.pro + - if %QSCINTILLA_BUILD%==1 if not %PLATFORM%==mingw (nmake) else (mingw32-make) + - if %DEPLOY%==1 if not %PLATFORM%==mingw (nmake install) else (mingw32-make install) + - if %DEPLOY%==1 cd "%APPVEYOR_BUILD_FOLDER%" + + # Build Grabber + - if %GRABBER_IS_NIGHTLY%==1 (set "BUILD_TYPE=Release") else (set "BUILD_TYPE=RelWithDebInfo") - mkdir build - cd build - - cmake .. -G "%MAKEFILES%" -DCMAKE_BUILD_TYPE=Release + - cmake .. -G "%MAKEFILES%" -DCMAKE_BUILD_TYPE=%BUILD_TYPE% -DNIGHTLY=%GRABBER_IS_NIGHTLY% -DCOMMIT="%APPVEYOR_REPO_COMMIT%" -DVERSION="%GRABBER_VERSION%" -DBREAKPAD="%APPVEYOR_BUILD_FOLDER%/breakpad/src" - if not %PLATFORM%==mingw (nmake) else (mingw32-make) - cd .. + + # Download APNG plugin DLL - if %DEPLOY%==1 if not exist %APNG_PLUGIN% curl -L https://install.skycoder42.de/qtmodules/windows_x86/qt5101/qt.qt5.5101.skycoder42.png.win32_msvc2015/1.1.05.10.1.7z -o "%APNG_PLUGIN%.7z" - if %DEPLOY%==1 if not exist %APNG_PLUGIN% 7z x "%APNG_PLUGIN%.7z" -y -o"%APNG_PLUGIN%" + + # Download Mysql DLL - if %DEPLOY%==1 if not exist %MYSQL_VERSION% curl -L -O "https://dev.mysql.com/get/Downloads/MySQL-5.7/%MYSQL_VERSION%.zip" - if %DEPLOY%==1 if not exist %MYSQL_VERSION% 7z x "%MYSQL_VERSION%.zip" -y - - if %DEPLOY%==1 iscc /Q /DMyAppVersion="nightly" /DQtDir="%QTDIR%\bin" /DOpenSSLDir="C:\OpenSSL-Win32" /DMySQLDir="%APPVEYOR_BUILD_FOLDER%\%MYSQL_VERSION%" /DQtApngDll="%APPVEYOR_BUILD_FOLDER%\%APNG_PLUGIN%\5.10.1\msvc2015\plugins\imageformats\qapng.dll" releases/setup.iss + + # Generate installer + - if %DEPLOY%==1 iscc /Q /DMyAppVersion="%GRABBER_VERSION%" /DPlatformName="%PLATFORM_NAME%" /DQtDir="%QTDIR%\bin" /DOpenSSLDir="%OPENSSLDIR%" /DMySQLDir="%APPVEYOR_BUILD_FOLDER%\%MYSQL_VERSION%" /DQtApngDll="%APPVEYOR_BUILD_FOLDER%\%APNG_PLUGIN%\5.10.1\msvc2015\plugins\imageformats\qapng.dll" releases/setup.iss + + # Package symbol files to zip + - if %DEPLOY%==1 if "%BUILD_TYPE%"=="RelWithDebInfo" 7z a "releases\Grabber_%GRABBER_VERSION%_%PLATFORM_NAME%_symbols.zip" ".\build\gui\Grabber.pdb" ".\build\cli\Grabber-cli.pdb" test_script: - build\tests\tests.exe artifacts: - - path: releases\Grabber_nightly.exe + - path: releases\Grabber_*.exe + name: windows_setup + - path: releases\Grabber_*_symbols.zip + name: windows_symbols_zip deploy: + # Nightly - provider: GitHub tag: nightly release: Nightly draft: false prerelease: true force_update: true - artifact: releases\Grabber_nightly.exe + artifact: windows_setup description: | Nightly automated builds from the develop branch. Automatically uploaded by AppVeyor, **use at your own risk**! @@ -76,3 +138,17 @@ deploy: on: branch: develop DEPLOY: 1 + + # Releases + - provider: GitHub + tag: $(APPVEYOR_REPO_TAG_NAME) + release: $(APPVEYOR_REPO_TAG_NAME) + draft: true + prerelease: false + force_update: true + artifact: windows_setup, windows_symbols_zip + auth_token: + secure: mUYQ72KBJUaYr5Bhy2HkBkEY13Q7k27Q7IRmOGXfTOq7YnXUS9PikETcZvzCfiVu + on: + APPVEYOR_REPO_TAG: true + DEPLOY: 1 diff --git a/.gitignore b/.gitignore index 796c134bc..cf243fa8b 100755 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ releases/release.sh releases/translation_tools.rar CrashReporter/debug/ CrashReporter/release/ +gui/android/AndroidManifest.xml # NPM @@ -131,3 +132,8 @@ Makefile* *.qmlproject.user *.qmlproject.user.* + + +# VS Code + +.vscode/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..abfdcc69a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "cmake/qt-android-cmake"] + path = cmake/qt-android-cmake + url = https://github.com/LaurentGomila/qt-android-cmake.git +[submodule "cmake/cotire"] + path = cmake/cotire + url = https://github.com/sakra/cotire.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9cbe3ca16..08c510301 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.6.0) +cmake_minimum_required(VERSION 3.2) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) @@ -19,28 +19,40 @@ if(MSVC OR CMAKE_CXX_COMPILER MATCHES "cl\\.exe") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") endif() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4714 /wd4127 /wd4005") # Suppress some warnings + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4714 /wd4127 /wd4005 /wd4251") # Suppress some warnings elseif(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-macro-redefined -pedantic") endif() project(Grabber) +if((NOT DEFINED VERSION) OR ((DEFINED NIGHTLY) AND (NIGHTLY MATCHES "1"))) + set(VERSION "7.0.0") +else() + string(REGEX REPLACE "^v" "" VERSION VERSION) +endif() set(USE_SSL 1) -set(USE_PCH 1) -add_definitions(-DVERSION="6.0.6") +# Android settings +set(ANDROID_APP_NAME ${PROJECT_NAME}) +set(ANDROID_APP_VERSION_NAME ${VERSION}) +set(ANDROID_APP_VERSION_CODE 1) +set(ANDROID_APP_PACKAGE_NAME "com.bionus.grabber") + +message(STATUS "Configuring for version '${VERSION}'") + +add_definitions(-DVERSION="${VERSION}") add_definitions(-DPROJECT_WEBSITE_URL="https://bionus.github.io/imgbrd-grabber") add_definitions(-DPROJECT_GITHUB_URL="https://github.com/Bionus/imgbrd-grabber") add_definitions(-DSOURCE_ISSUES_URL="https://raw.githubusercontent.com/wiki/Bionus/imgbrd-grabber/SourceIssues.md") add_definitions(-DPREFIX="${CMAKE_INSTALL_PREFIX}") # SSL -if(USE_SSL) - message("Compiling with SSL support") +if(USE_SSL AND (NOT ANDROID)) + message(STATUS "Compiling with SSL support") find_package(OpenSSL REQUIRED) if (OPENSSL_FOUND) - message("OpenSSL include dir: ${OPENSSL_INCLUDE_DIR}") - message("OpenSSL libraries: ${OPENSSL_LIBRARIES}") + message(STATUS "OpenSSL include dir: ${OPENSSL_INCLUDE_DIR}") + message(STATUS "OpenSSL libraries: ${OPENSSL_LIBRARIES}") include_directories(${OPENSSL_INCLUDE_DIR}) list(APPEND LIBS ${OPENSSL_LIBRARIES}) endif() @@ -53,14 +65,14 @@ if((DEFINED ENV{COVERAGE}) AND CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 -fprofile-arcs -ftest-coverage") endif() -# Add a bunch of definitions for Travis builds -if((DEFINED ENV{TRAVIS}) AND UNIX AND NOT APPLE AND CMAKE_COMPILER_IS_GNUCXX) - add_definitions(-DTRAVIS=1) - if("$ENV{TRAVIS_OS_NAME}" STREQUAL "osx") - add_definitions(-DTRAVIS_OS_OSX=1) - elseif("$ENV{TRAVIS_OS_NAME}" STREQUAL "linux") - add_definitions(-DTRAVIS_OS_LINUX=1) - endif() +# Pre-compiled headers +if(WIN32) + set(USE_PCH 1) +else() + set(USE_PCH 0) +endif() +if(USE_PCH) + include(cotire/CMake/cotire) endif() add_subdirectory(lib) @@ -77,11 +89,21 @@ install(DIRECTORY release/ DESTINATION share/Grabber/ PATTERN "*.exe" EXCLUDE) install(FILES "release/Grabber.desktop" DESTINATION share/applications/) +# Replicate the behavior of "list(FILTER ${LIST} EXCLUDE REGEX ${REGEX})" on older CMake versions +function(listFilterRegex LIST REGEX) + foreach(ITEM ${${LIST}}) + if(NOT ITEM MATCHES "${REGEX}") + list(APPEND OUTPUT ${ITEM}) + endif() + endforeach() + set(${LIST} ${OUTPUT} PARENT_SCOPE) +endfunction() + # Transpile TS sites into JS set(SITES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/release/sites") file(GLOB_RECURSE SITES_TS_FILES "${SITES_DIR}/**.ts") -list(FILTER SITES_TS_FILES EXCLUDE REGEX ".*.d.ts$") -list(FILTER SITES_TS_FILES EXCLUDE REGEX "node_modules") +listFilterRegex(SITES_TS_FILES ".*.d.ts$") +listFilterRegex(SITES_TS_FILES "node_modules") add_custom_command( OUTPUT NPM_modules DEPENDS "${SITES_DIR}/package.json" diff --git a/README.md b/README.md index 0eedb1643..4d2d68396 100755 --- a/README.md +++ b/README.md @@ -81,17 +81,17 @@ For users interested, a nightly version is built automatically on every commit o * Rename already downloaded images ## Default sources -You can add additional sources very easily, but here's a short list of some sources that are included and supported by default: Danbooru, Gelbooru, yande.re, Shimmie, e621, Konachan, rule34, safebooru, behoimi, Zerochan... +You can add additional sources very easily, but here's a short list of some sources that are included and supported by default: Danbooru, Gelbooru, yande.re, Shimmie, e621, Konachan, rule34, safebooru, behoimi, Zerochan, Twitter... ## Compilation See the [Compilation](https://github.com/Bionus/imgbrd-grabber/wiki/Compilation) wiki page to know how to build Grabber. ## Contributors - - -| [
Jack Vasti](https://github.com/Bionus)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Code") [📖](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Documentation") [⚠️](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Tests") | [
YMI](https://github.com/Zzzyyzzyxx)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AYMI "Bug reports") [🤔](#ideas-YMI "Ideas, Planning, & Feedback") | [
SultrySamthepenna…](https://github.com/SultrySamthepennanceman)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3ASultrySamthepennanceman "Bug reports") | [
Barry Anders](https://github.com/BarryMode)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=BarryMode "Code") [🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3ABarryMode "Bug reports") | [
Ken Swenson](https://github.com/Flat)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=Flat "Code") [📦](#platform-Flat "Packaging/porting to new platform") | [
Larry He](https://github.com/larry-he)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=larry-he "Code") | -| :---: | :---: | :---: | :---: | :---: | :---: | -| [
brodycas3](https://github.com/brodycas3)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3Abrodycas3 "Bug reports") | [
Klion Xu](https://github.com/sanddudu)
[🌍](#translation-sanddudu "Translation") | [
MasterPetrik](https://github.com/MasterPetrik)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AMasterPetrik "Bug reports") [🌍](#translation-MasterPetrik "Translation") [🤔](#ideas-MasterPetrik "Ideas, Planning, & Feedback") | [
Eddy Castillo](https://github.com/dyskette)
[🌍](#translation-dyskette "Translation") | [
MrAndre96](https://github.com/MrAndre96)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AMrAndre96 "Bug reports") | + + +| [
Jack Vasti](https://github.com/Bionus)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Code") [📖](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Documentation") [⚠️](https://github.com/Bionus/imgbrd-grabber/commits?author=Bionus "Tests") | [
YMI](https://github.com/Zzzyyzzyxx)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AYMI "Bug reports") [🤔](#ideas-YMI "Ideas, Planning, & Feedback") | [
SultrySamthepenna…](https://github.com/SultrySamthepennanceman)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3ASultrySamthepennanceman "Bug reports") | [
Barry Anders](https://github.com/BarryMode)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=BarryMode "Code") [🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3ABarryMode "Bug reports") | [
Ken Swenson](https://github.com/Flat)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=Flat "Code") [📦](#platform-Flat "Packaging/porting to new platform") | [
Larry He](https://github.com/larry-he)
[💻](https://github.com/Bionus/imgbrd-grabber/commits?author=larry-he "Code") | +| :---: | :---: | :---: | :---: | :---: | :---: | +| [
brodycas3](https://github.com/brodycas3)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3Abrodycas3 "Bug reports") | [
Klion Xu](https://github.com/sanddudu)
[🌍](#translation-sanddudu "Translation") | [
MasterPetrik](https://github.com/MasterPetrik)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AMasterPetrik "Bug reports") [🌍](#translation-MasterPetrik "Translation") [🤔](#ideas-MasterPetrik "Ideas, Planning, & Feedback") | [
Eddy Castillo](https://github.com/dyskette)
[🌍](#translation-dyskette "Translation") | [
MrAndre96](https://github.com/MrAndre96)
[🐛](https://github.com/Bionus/imgbrd-grabber/issues?q=author%3AMrAndre96 "Bug reports") | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 185e4ce92..a1843f74e 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -3,7 +3,8 @@ project(cli) add_definitions(-DCLI=1) find_package(Qt5Core REQUIRED) -set(QT_LIBRARIES Qt5::Core) +find_package(Qt5Network REQUIRED) +set(QT_LIBRARIES Qt5::Core Qt5::Network) file(GLOB_RECURSE SOURCES "src/*.cpp") include_directories("src/" "../lib/src/" "..") diff --git a/cli/src/main.cpp b/cli/src/main.cpp index 064d064c0..335ec8ae4 100644 --- a/cli/src/main.cpp +++ b/cli/src/main.cpp @@ -1,6 +1,12 @@ #include +#include +#include +#include +#include #include "downloader/downloader.h" #include "functions.h" +#include "logger.h" +#include "models/filtering/blacklist.h" #include "models/profile.h" #include "models/site.h" #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) @@ -9,6 +15,42 @@ #include #endif +void loadMoreDetails(const QList> &images); +QJsonObject serializeImg(const Image *image, const QMap& tokens); +void writeToFile(QString filename, QByteArray data); + +void returnJsonArray(const QJsonArray &array) +{ + QJsonDocument jsonDoc; + jsonDoc.setArray(array); + + QByteArray jsonResult = jsonDoc.toJson(QJsonDocument::Indented); + QTextStream(stdout) << qPrintable(jsonResult); + + qApp->quit(); +} + +void serializeTags(const QList &tags) +{ + QJsonArray jsonArray; + for (const Tag &tag : tags) { + QJsonObject jsonObj; + tag.write(jsonObj); + jsonArray.append(jsonObj); + } + returnJsonArray(jsonArray); +} + +void serializeImages(Profile *profile, const QList> &images) +{ + QJsonArray jsonArray; + for (const auto &image : images) { + auto tokens = image->tokens(profile); + auto jsObject = serializeImg(image.data(), tokens); + jsonArray.append(jsObject); + } + returnJsonArray(jsonArray); +} int main(int argc, char *argv[]) { @@ -32,11 +74,18 @@ int main(int argc, char *argv[]) const QCommandLineOption userOption(QStringList() << "u" << "user", "Username to connect to the source.", "user"); const QCommandLineOption passwordOption(QStringList() << "w" << "password", "Password to connect to the source.", "password"); const QCommandLineOption blacklistOption(QStringList() << "b" << "blacklist", "Download blacklisted images."); + const QCommandLineOption tagsBlacklistOption(QStringList() << "tb" << "tags-blacklist" , "Tags to remove from results.", "tags-blacklist"); const QCommandLineOption postFilteringOption(QStringList() << "r" << "postfilter", "Filter results.", "filter"); const QCommandLineOption noDuplicatesOption(QStringList() << "n" << "no-duplicates", "Remove duplicates from results."); const QCommandLineOption verboseOption(QStringList() << "d" << "debug", "Show debug messages."); const QCommandLineOption tagsMinOption(QStringList() << "tm" << "tags-min", "Minimum count for tags to be returned.", "count", "0"); const QCommandLineOption tagsFormatOption(QStringList() << "tf" << "tags-format", "Format for returning tags.", "format", "%tag\t%count\t%type"); + const QCommandLineOption ignoreErrorOption(QStringList() << "ignore-error", "don't exit on error."); + const QCommandLineOption proxyOption(QStringList() << "proxy", "Use given proxy.", "[user:password]@host:port", ""); + const QCommandLineOption noLoginOption(QStringList() << "no-login", "disable auto login."); + const QCommandLineOption jsonOption(QStringList() << "j" << "json", "output results as json."); + const QCommandLineOption loadDetailsOption(QStringList() << "load-details", "request (more) details on found items."); + const QCommandLineOption getDetailsOption(QStringList() << "get-details", "parse details from given link.", "url-page"); parser.addOption(tagsOption); parser.addOption(sourceOption); parser.addOption(pageOption); @@ -47,11 +96,18 @@ int main(int argc, char *argv[]) parser.addOption(userOption); parser.addOption(passwordOption); parser.addOption(blacklistOption); + parser.addOption(tagsBlacklistOption); parser.addOption(postFilteringOption); parser.addOption(tagsMinOption); parser.addOption(tagsFormatOption); parser.addOption(noDuplicatesOption); parser.addOption(verboseOption); + parser.addOption(ignoreErrorOption); + parser.addOption(proxyOption); + parser.addOption(noLoginOption); + parser.addOption(jsonOption); + parser.addOption(loadDetailsOption); + parser.addOption(getDetailsOption); const QCommandLineOption returnCountOption(QStringList() << "rc" << "return-count", "Return total image count."); const QCommandLineOption returnTagsOption(QStringList() << "rt" << "return-tags", "Return tags for a search."); const QCommandLineOption returnPureTagsOption(QStringList() << "rp" << "return-pure-tags", "Return tags."); @@ -65,17 +121,87 @@ int main(int argc, char *argv[]) parser.process(app); - #ifndef QT_DEBUG - Logger::setupMessageOutput(parser.isSet(verboseOption)); - #endif + if (parser.isSet(verboseOption)) { + #ifndef QT_DEBUG + Logger::getInstance().logToConsole(); + #endif + Logger::getInstance().setLogLevel(Logger::Debug); + } + + if (!parser.isSet(ignoreErrorOption)) { + Logger::getInstance().setExitOnError(true); + } + + if (parser.isSet(proxyOption)) { + QUrl proxyUrl = QUrl::fromUserInput(parser.value(proxyOption)); + + if (!proxyUrl.isValid()) { + log(proxyUrl.errorString(), Logger::Error); + } + if (proxyUrl.port() == -1) { + log("Bad proxy port.", Logger::Error); + } + + const auto type = proxyUrl.scheme().startsWith("socks") + ? QNetworkProxy::Socks5Proxy + : QNetworkProxy::HttpProxy; + + const QNetworkProxy proxy( + type, + proxyUrl.host(), + proxyUrl.port(), + proxyUrl.userName(), + proxyUrl.password() + ); + + QNetworkProxy::setApplicationProxy(proxy); + log(QStringLiteral("Enabling application proxy on host \"%1\" and port %2.").arg(proxyUrl.host()).arg(proxyUrl.port()), Logger::Info); + } Profile *profile = new Profile(savePath()); profile->purgeTemp(24 * 60 * 60); + auto sites = profile->getFilteredSites(parser.value(sourceOption).split(" ", QString::SkipEmptyParts)); + if (parser.isSet(noLoginOption)) { + for (auto& site : sites) { + site->setAutoLogin(false); + } + } + + if (parser.isSet(getDetailsOption)) { + if (sites.length() != 1) { + throw std::runtime_error("number of provided sites must be 1"); + } + if (!parser.isSet(verboseOption)) { + Logger::getInstance().setLogLevel(Logger::Error); + } + + QString detailsUrl = parser.value(getDetailsOption); + QMap details = {{"page_url", detailsUrl}}; + Image image(sites[0], details, profile); + image.setPromoteDetailParsWarn(true); + + QEventLoop loop; + image.loadDetails(); + QObject::connect(&image, &Image::finishedLoadingTags, &loop, &QEventLoop::quit); + loop.exec(); + + auto tokens = image.tokens(profile); + auto jsObject = serializeImg(&image, tokens); + + QJsonDocument jsonDoc; + jsonDoc.setObject(jsObject); + auto jsonResult = jsonDoc.toJson(QJsonDocument::Indented); + QTextStream(stdout) << qPrintable(jsonResult); + + exit(0); + } + + QString blacklistOverride = parser.value(tagsBlacklistOption); Downloader *downloader = new Downloader(profile, parser.value(tagsOption).split(" ", QString::SkipEmptyParts), parser.value(postFilteringOption).split(" ", QString::SkipEmptyParts), - profile->getFilteredSites(parser.value(sourceOption).split(" ", QString::SkipEmptyParts)), + sites, parser.value(pageOption).toInt(), parser.value(limitOption).toInt(), parser.value(perPageOption).toInt(), @@ -84,26 +210,122 @@ int main(int argc, char *argv[]) parser.value(userOption), parser.value(passwordOption), parser.isSet(blacklistOption), - profile->getBlacklist(), + blacklistOverride.isEmpty() ? profile->getBlacklist() : Blacklist(blacklistOverride.split(' ')), parser.isSet(noDuplicatesOption), parser.value(tagsMinOption).toInt(), parser.value(tagsFormatOption)); - if (parser.isSet(returnCountOption)) + downloader->setQuit(true); + + // JSON output + if (parser.isSet(jsonOption)) { + downloader->setQuit(false); + + QObject::connect(downloader, &Downloader::finishedTags, serializeTags); + QObject::connect(downloader, &Downloader::finishedImages, [&](const QList> &images) { + if (parser.isSet(loadDetailsOption)) { + loadMoreDetails(images); + } + serializeImages(profile, images); + }); + } + + // Load the correct data + if (parser.isSet(returnCountOption)) { downloader->getPageCount(); - else if (parser.isSet(returnTagsOption)) + } else if (parser.isSet(returnTagsOption)) { downloader->getPageTags(); - else if (parser.isSet(returnPureTagsOption)) + } else if (parser.isSet(returnPureTagsOption)) { downloader->getTags(); - else if (parser.isSet(returnImagesOption)) + } else if (parser.isSet(returnImagesOption)) { downloader->getUrls(); - else if (parser.isSet(downloadOption)) + } else if (parser.isSet(downloadOption) || parser.isSet(jsonOption)) { downloader->getImages(); - else + } else { parser.showHelp(); + } - downloader->setQuit(true); QObject::connect(downloader, &Downloader::quit, qApp, &QCoreApplication::quit); return app.exec(); } + +void loadMoreDetails(const QList> &images) +{ + int work = images.length(); + QEventLoop loop; + int requestsLimit = 5; // simultan requests + int runningRequests = 0; + for (auto& image : images) { + while (runningRequests >= requestsLimit) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + runningRequests++; + image->loadDetails(); + QObject::connect(image.data(), &Image::finishedLoadingTags, [&](){ + work--; + runningRequests--; + if (!work) { + loop.quit(); + } + }); + } + loop.exec(); +} + +QJsonObject serializeImg(const Image *image, const QMap &tokens) +{ + static QStringList ignoreKeys = {"all", "allo", "allos", "all_namespaces", }; + QJsonObject jsObject; + + for (auto& key : tokens.keys()) { + typedef QVariant::Type Type; + if (ignoreKeys.contains(key)) { + continue; + } + if (key.contains("search_")) { + continue; + } + + const QVariant& qvalue = tokens.value(key).value(); + auto type = qvalue.type(); + + if (type == QVariant::Type::StringList) { + QStringList l = qvalue.toStringList(); + if (l.isEmpty()) { + continue; + } + jsObject.insert(key, QJsonArray::fromStringList(l)); + } else if (type == QVariant::Type::String) { + QString s = qvalue.toString(); + if (s.isEmpty()) { + continue; + } + jsObject.insert(key, s); + } else if (type == Type::Url || type == Type::ULongLong || type == Type::LongLong) { + jsObject.insert(key, qvalue.toString()); + } else if (type == Type::Int) { + jsObject.insert(key, qvalue.value()); + } else if (type == Type::Bool) { + jsObject.insert(key, qvalue.value()); + } else if (type == Type::DateTime) { + jsObject.insert(key, static_cast(qvalue.value().toTime_t())); + } else { + qDebug() << qvalue; + log(QStringLiteral("using generic QVariant::toString for key: %1").arg(key), Logger::Warning); + jsObject.insert(key, qvalue.toString()); + } + } + jsObject.insert("isVideo", image->isVideo()); + jsObject.insert("isGallery", image->isGallery()); + jsObject.insert("isAnimated", image->isAnimated()); + return jsObject; +} + +void writeToFile(QString filename, QByteArray data) +{ + QFile file(filename); + file.open(QFile::WriteOnly); + file.write(data); + file.close(); +} diff --git a/cmake/PrecompiledHeader.cmake b/cmake/PrecompiledHeader.cmake deleted file mode 100644 index 9447eb881..000000000 --- a/cmake/PrecompiledHeader.cmake +++ /dev/null @@ -1,237 +0,0 @@ -# Function for setting up precompiled headers. Usage: -# -# add_library/executable(target -# pchheader.c pchheader.cpp pchheader.h) -# -# add_precompiled_header(target pchheader.h -# [FORCEINCLUDE] -# [SOURCE_C pchheader.c] -# [SOURCE_CXX pchheader.cpp]) -# -# Options: -# -# FORCEINCLUDE: Add compiler flags to automatically include the -# pchheader.h from every source file. Works with both GCC and -# MSVC. This is recommended. -# -# SOURCE_C/CXX: Specifies the .c/.cpp source file that includes -# pchheader.h for generating the pre-compiled header -# output. Defaults to pchheader.c. Only required for MSVC. -# -# Caveats: -# -# * Its not currently possible to use the same precompiled-header in -# more than a single target in the same directory (No way to set -# the source file properties differently for each target). -# -# * MSVC: A source file with the same name as the header must exist -# and be included in the target (E.g. header.cpp). Name of file -# can be changed using the SOURCE_CXX/SOURCE_C options. -# -# License: -# -# Copyright (C) 2009-2017 Lars Christensen -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the 'Software') 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. - -include(CMakeParseArguments) - -macro(combine_arguments _variable) - set(_result "") - foreach(_element ${${_variable}}) - set(_result "${_result} \"${_element}\"") - endforeach() - string(STRIP "${_result}" _result) - set(${_variable} "${_result}") -endmacro() - -function(export_all_flags _filename) - set(_include_directories "$") - set(_compile_definitions "$") - set(_compile_flags "$") - set(_compile_options "$") - set(_include_directories "$<$:-I$\n>") - set(_compile_definitions "$<$:-D$\n>") - set(_compile_flags "$<$:$\n>") - set(_compile_options "$<$:$\n>") - - # CUSTOM:start - get_target_property(_target_type ${_target} TYPE) - if(_target_type STREQUAL "EXECUTABLE") - set(_pic "$<$>:-fPIE\n>") - else() - set(_pic "$<$>:-fPIC\n>") - endif() - - set(_standard_check "") - set(_cxx_standard "$") - set(_cxx_extensions "$") - set(_has_extensions "$,$>") # CXX_EXTENSIONS defaults to true - set(_standard_check "${_standard_check}$<$,$>:${CMAKE_CXX17_STANDARD_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,$>:${CMAKE_CXX14_STANDARD_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,$>:${CMAKE_CXX11_STANDARD_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,$>:${CMAKE_CXX98_STANDARD_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,${_has_extensions}>:${CMAKE_CXX17_EXTENSION_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,${_has_extensions}>:${CMAKE_CXX14_EXTENSION_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,${_has_extensions}>:${CMAKE_CXX11_EXTENSION_COMPILE_OPTION}>") - set(_standard_check "${_standard_check}$<$,${_has_extensions}>:${CMAKE_CXX98_EXTENSION_COMPILE_OPTION}>") - - file(GENERATE OUTPUT "${_filename}" CONTENT "${_compile_definitions}${_include_directories}${_compile_flags}${_compile_options} ${_standard_check} ${_pic}\n") - # CUSTOM:end -endfunction() - -function(add_precompiled_header _target _input) - cmake_parse_arguments(_PCH "FORCEINCLUDE" "SOURCE_CXX;SOURCE_C" "" ${ARGN}) - - get_filename_component(_input_we ${_input} NAME_WE) - if(NOT _PCH_SOURCE_CXX) - set(_PCH_SOURCE_CXX "${_input_we}.cpp") - endif() - if(NOT _PCH_SOURCE_C) - set(_PCH_SOURCE_C "${_input_we}.c") - endif() - - if(MSVC) - set(_pch_cxx_pch "${CMAKE_CFG_INTDIR}/cxx_${_input_we}.pch") - set(_pch_c_pch "${CMAKE_CFG_INTDIR}/c_${_input_we}.pch") - - get_target_property(sources ${_target} SOURCES) - foreach(_source ${sources}) - set(_pch_compile_flags "") - if(_source MATCHES \\.\(cc|cxx|cpp|c\)$) - if(_source MATCHES \\.\(cpp|cxx|cc\)$) - set(_pch_header "${_input}") - set(_pch "${_pch_cxx_pch}") - else() - set(_pch_header "${_input}") - set(_pch "${_pch_c_pch}") - endif() - - if(_source STREQUAL "${_PCH_SOURCE_CXX}") - set(_pch_compile_flags "${_pch_compile_flags} \"/Fp${_pch_cxx_pch}\" \"/Yc${_input}\"") - set(_pch_source_cxx_found TRUE) - set_source_files_properties("${_source}" PROPERTIES OBJECT_OUTPUTS "${_pch_cxx_pch}") - elseif(_source STREQUAL "${_PCH_SOURCE_C}") - set(_pch_compile_flags "${_pch_compile_flags} \"/Fp${_pch_c_pch}\" \"/Yc${_input}\"") - set(_pch_source_c_found TRUE) - set_source_files_properties("${_source}" PROPERTIES OBJECT_OUTPUTS "${_pch_c_pch}") - else() - if(_source MATCHES \\.\(cpp|cxx|cc\)$) - set(_pch_compile_flags "${_pch_compile_flags} \"/Fp${_pch_cxx_pch}\" \"/Yu${_input}\"") - set(_pch_source_cxx_needed TRUE) - set_source_files_properties("${_source}" PROPERTIES OBJECT_DEPENDS "${_pch_cxx_pch}") - else() - set(_pch_compile_flags "${_pch_compile_flags} \"/Fp${_pch_c_pch}\" \"/Yu${_input}\"") - set(_pch_source_c_needed TRUE) - set_source_files_properties("${_source}" PROPERTIES OBJECT_DEPENDS "${_pch_c_pch}") - endif() - if(_PCH_FORCEINCLUDE) - set(_pch_compile_flags "${_pch_compile_flags} /FI${_input}") - endif(_PCH_FORCEINCLUDE) - endif() - - get_source_file_property(_object_depends "${_source}" OBJECT_DEPENDS) - if(NOT _object_depends) - set(_object_depends) - endif() - if(_PCH_FORCEINCLUDE) - list(APPEND _object_depends "${CMAKE_CURRENT_SOURCE_DIR}/${_pch_header}") - endif() - - set_source_files_properties(${_source} PROPERTIES - COMPILE_FLAGS "${_pch_compile_flags}" - OBJECT_DEPENDS "${_object_depends}") - endif() - endforeach() - - if(_pch_source_cxx_needed AND NOT _pch_source_cxx_found) - message(FATAL_ERROR "A source file ${_PCH_SOURCE_CXX} for ${_input} is required for MSVC builds. Can be set with the SOURCE_CXX option.") - endif() - if(_pch_source_c_needed AND NOT _pch_source_c_found) - message(FATAL_ERROR "A source file ${_PCH_SOURCE_C} for ${_input} is required for MSVC builds. Can be set with the SOURCE_C option.") - endif() - endif(MSVC) - - if(CMAKE_COMPILER_IS_GNUCXX) - get_filename_component(_name ${_input} NAME) - set(_pch_header "${CMAKE_CURRENT_SOURCE_DIR}/${_input}") - set(_pch_binary_dir "${CMAKE_CURRENT_BINARY_DIR}/${_target}_pch") - set(_pchfile "${_pch_binary_dir}/${_input}") - set(_outdir "${CMAKE_CURRENT_BINARY_DIR}/${_target}_pch/${_name}.gch") - file(MAKE_DIRECTORY "${_outdir}") - set(_output_cxx "${_outdir}/.c++") - set(_output_c "${_outdir}/.c") - - set(_pch_flags_file "${_pch_binary_dir}/compile_flags.rsp") - export_all_flags("${_pch_flags_file}") - set(_compiler_FLAGS "@${_pch_flags_file}") - add_custom_command( - OUTPUT "${_pchfile}" - COMMAND "${CMAKE_COMMAND}" -E copy "${_pch_header}" "${_pchfile}" - DEPENDS "${_pch_header}" - COMMENT "Updating ${_name}") - add_custom_command( - OUTPUT "${_output_cxx}" - COMMAND "${CMAKE_CXX_COMPILER}" ${_compiler_FLAGS} -x c++-header -o "${_output_cxx}" "${_pchfile}" - DEPENDS "${_pchfile}" "${_pch_flags_file}" - COMMENT "Precompiling ${_name} for ${_target} (C++)") - add_custom_command( - OUTPUT "${_output_c}" - COMMAND "${CMAKE_C_COMPILER}" ${_compiler_FLAGS} -x c-header -o "${_output_c}" "${_pchfile}" - DEPENDS "${_pchfile}" "${_pch_flags_file}" - COMMENT "Precompiling ${_name} for ${_target} (C)") - - get_property(_sources TARGET ${_target} PROPERTY SOURCES) - foreach(_source ${_sources}) - set(_pch_compile_flags "") - - if(_source MATCHES \\.\(cc|cxx|cpp|c\)$) - get_source_file_property(_pch_compile_flags "${_source}" COMPILE_FLAGS) - if(NOT _pch_compile_flags) - set(_pch_compile_flags) - endif() - separate_arguments(_pch_compile_flags) - list(APPEND _pch_compile_flags -Winvalid-pch) - if(_PCH_FORCEINCLUDE) - list(APPEND _pch_compile_flags -include "${_pchfile}") - else(_PCH_FORCEINCLUDE) - list(APPEND _pch_compile_flags "-I${_pch_binary_dir}") - endif(_PCH_FORCEINCLUDE) - - get_source_file_property(_object_depends "${_source}" OBJECT_DEPENDS) - if(NOT _object_depends) - set(_object_depends) - endif() - list(APPEND _object_depends "${_pchfile}") - if(_source MATCHES \\.\(cc|cxx|cpp\)$) - list(APPEND _object_depends "${_output_cxx}") - else() - list(APPEND _object_depends "${_output_c}") - endif() - - combine_arguments(_pch_compile_flags) - set_source_files_properties(${_source} PROPERTIES - COMPILE_FLAGS "${_pch_compile_flags}" - OBJECT_DEPENDS "${_object_depends}") - endif() - endforeach() - endif(CMAKE_COMPILER_IS_GNUCXX) -endfunction() diff --git a/cmake/cotire b/cmake/cotire new file mode 160000 index 000000000..391bf6b76 --- /dev/null +++ b/cmake/cotire @@ -0,0 +1 @@ +Subproject commit 391bf6b7609e14f5976bd5247b68d63cbf8d4d12 diff --git a/cmake/qt-android-cmake b/cmake/qt-android-cmake new file mode 160000 index 000000000..081ee8424 --- /dev/null +++ b/cmake/qt-android-cmake @@ -0,0 +1 @@ +Subproject commit 081ee8424a43f79f7ec0fa07223647b1925f35e5 diff --git a/e2e/src/main.cpp b/e2e/src/main.cpp index f73dd9b25..7c81f377f 100644 --- a/e2e/src/main.cpp +++ b/e2e/src/main.cpp @@ -18,27 +18,30 @@ bool opCompare(const QString &op, int left, int right) { - if (right == -1) + if (right == -1) { return true; - if (op == ">") + } + if (op == ">") { return left > right; - if (op == "<") + } + if (op == "<") { return left < right; + } return left == right; } bool jsonCompare(const QVariant &value, QJsonValue opt) { QString op = "="; - if (opt.isArray()) - { + if (opt.isArray()) { QJsonArray arrOpt = opt.toArray(); op = arrOpt[0].toString(); opt = arrOpt[1]; } - if (value.type() == QVariant::String) - { return value.toString() == opt.toString(); } + if (value.type() == QVariant::String) { + return value.toString() == opt.toString(); + } return opCompare(op, value.toInt(), opt.toDouble()); } @@ -59,8 +62,9 @@ int main(int argc, char *argv[]) Logger::getInstance().setLogLevel(Logger::Warning); QFile f(parser.value(inputOption)); - if (!f.open(QFile::ReadOnly | QFile::Text)) + if (!f.open(QFile::ReadOnly | QFile::Text)) { return 1; + } QJsonObject allJson; QJsonDocument input = QJsonDocument::fromJson(f.readAll()); @@ -78,8 +82,7 @@ int main(int argc, char *argv[]) const QJsonArray rootSearch = root.value("search").toArray(); const QJsonObject sources = root.value("sources").toObject(); - for (auto it = sources.constBegin(); it != sources.constEnd(); ++it) - { + for (auto it = sources.constBegin(); it != sources.constEnd(); ++it) { const QString &sourceName = it.key(); qDebug() << "#" << "Source" << sourceName; QJsonObject sourceJson; @@ -89,46 +92,50 @@ int main(int argc, char *argv[]) const QJsonObject sourceApis = sites.value("apis").toObject(); QJsonArray sourceSearch = rootSearch; - if (sites.contains("search")) - { sourceSearch = sites.value("search").toArray(); } + if (sites.contains("search")) { + sourceSearch = sites.value("search").toArray(); + } - for (Site *site : source->getSites()) - { + for (Site *site : source->getSites()) { qDebug() << "##" << "Site" << site->url(); QJsonObject siteJson; QJsonObject siteApis = sourceApis; QJsonArray siteSearch = sourceSearch; - if (sites.contains(site->url())) - { + if (sites.contains(site->url())) { QJsonObject override = sites.value(site->url()).toObject(); - if (override.contains("apis")) - { siteApis = override.value("apis").toObject(); } - if (override.contains("search")) - { siteSearch = override.value("search").toArray(); } + if (override.contains("apis")) { + siteApis = override.value("apis").toObject(); + } + if (override.contains("search")) { + siteSearch = override.value("search").toArray(); + } } - for (auto ita = siteApis.constBegin(); ita != siteApis.constEnd(); ++ita) - { + for (auto ita = siteApis.constBegin(); ita != siteApis.constEnd(); ++ita) { const QString &apiName = ita.key(); qDebug() << "###" << "API" << apiName; QJsonObject apiJson; QJsonArray checks = ita.value().toArray(); QJsonArray apiSearch = siteSearch; - if (checks.count() > 4) - { apiSearch = checks[4].toArray(); } + if (checks.count() > 4) { + apiSearch = checks[4].toArray(); + } const QString search = apiSearch[0].toString(); const int pagei = apiSearch[1].toDouble(); const int limit = apiSearch[2].toDouble(); Api *api = nullptr; - for (Api *a : site->getApis()) - if (a->getName().toLower() == apiName.toLower()) + for (Api *a : site->getApis()) { + if (a->getName().toLower() == apiName.toLower()) { api = a; - if (api == nullptr) + } + } + if (api == nullptr) { continue; + } auto page = new Page(profile, site, allSites.values(), QStringList() << search, pagei, limit); auto pageApi = new PageApi(page, profile, site, api, search.split(' '), pagei, limit); @@ -141,39 +148,37 @@ int main(int argc, char *argv[]) QStringList message; // Checks - if (!jsonCompare(pageApi->errors().join(", "), checks[0])) - { + if (!jsonCompare(pageApi->errors().join(", "), checks[0])) { apiJson["status"] = "error"; message.append(pageApi->errors()); } - if (!jsonCompare(pageApi->imagesCount(false), checks[1])) - { - if (apiJson["status"] == "ok") - { apiJson["status"] = "warning"; } + if (!jsonCompare(pageApi->imagesCount(false), checks[1])) { + if (apiJson["status"] == "ok") { + apiJson["status"] = "warning"; + } message.append("Image count error: " + QString::number(pageApi->imagesCount(false))); } - if (!jsonCompare(pageApi->images().count(), checks[2])) - { + if (!jsonCompare(pageApi->images().count(), checks[2])) { apiJson["status"] = "error"; message.append("Number of images error: " + QString::number(pageApi->images().count())); } - if (!jsonCompare(pageApi->tags().count(), checks[3])) - { - if (apiJson["status"] == "ok") - { apiJson["status"] = "warning"; } + if (!jsonCompare(pageApi->tags().count(), checks[3])) { + if (apiJson["status"] == "ok") { + apiJson["status"] = "warning"; + } QStringList tags; - for (const Tag& tag : pageApi->tags()) - { tags.append(tag.text()); } + for (const Tag& tag : pageApi->tags()) { + tags.append(tag.text()); + } message.append("Number of tags error: " + QString::number(pageApi->tags().count()) + " [" + tags.join(", ") + "]"); } - if (!message.isEmpty()) - { + if (!message.isEmpty()) { apiJson["message"] = message.join(", "); siteJson[apiName] = apiJson; + } else { + siteJson[apiName] = apiJson["status"]; } - else - { siteJson[apiName] = apiJson["status"]; } pageApi->deleteLater(); page->deleteLater(); @@ -189,8 +194,9 @@ int main(int argc, char *argv[]) QJsonDocument outDoc(allJson); QFile fOut(parser.value(outputOption)); - if (!fOut.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) + if (!fOut.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { return 1; + } fOut.write(outDoc.toJson()); fOut.close(); diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 46aaca665..17a0e30ed 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -6,16 +6,23 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release") set(USE_QSCINTILLA 1) endif() -# Continuous integration settings -if(DEFINED ENV{TRAVIS} OR DEFINED ENV{APPVEYOR}) +# Disable Google Breakpad and QScintilla on Linux builds +if(DEFINED ENV{TRAVIS}) set(USE_BREAKPAD 0) set(USE_QSCINTILLA 0) endif() +# Disable Google Breakpad if the provided folder does not exist +if((DEFINED BREAKPAD) AND (NOT EXISTS "${BREAKPAD}")) + message(WARNING "Provided Google Breakpad directory does not exist, disabling Google Breakpad: ${BREAKPAD}") + set(USE_BREAKPAD 0) +endif() + # Nightly version settings -if (DEFINED ENV{APPVEYOR}) +if((DEFINED NIGHTLY) AND (NIGHTLY MATCHES "1")) add_definitions(-DNIGHTLY=1) - add_definitions(-DNIGHTLY_COMMIT="$ENV{APPVEYOR_REPO_COMMIT}") + add_definitions(-DNIGHTLY_COMMIT="${COMMIT}") + message(STATUS "Configuring nightly with commit '${COMMIT}'") endif() # Qt libraries @@ -35,14 +42,21 @@ if(WIN32) set(QT_LIBRARIES ${QT_LIBRARIES} Qt5::WinExtras) endif() +# Android specials +if(ANDROID) + find_package(Qt5AndroidExtras REQUIRED) + set(QT_LIBRARIES ${QT_LIBRARIES} Qt5::AndroidExtras) +endif() + # QScintilla if(USE_QSCINTILLA) - find_package(QScintilla REQUIRED) + find_package(QScintilla) if(QSCINTILLA_FOUND) + message(STATUS "Building with QScintilla2 support") add_definitions(-DUSE_QSCINTILLA=1) list(APPEND LIBS ${QSCINTILLA_LIBRARY}) else() - MESSAGE(INFO "QScintilla2 not found") + message(WARNING "QScintilla2 not found") endif() endif() @@ -76,7 +90,9 @@ if(USE_BREAKPAD) if(WIN32) set(CMAKE_LFLAGS_RELEASE "${CMAKE_LFLAGS_RELEASE} /INCREMENTAL:NO /DEBUG") set(CMAKE_CFLAGS_RELEASE "${CMAKE_CFLAGS_RELEASE} -O2 -MD -zi") - set(BREAKPAD "D:/bin/google-breakpad") + if(NOT DEFINED BREAKPAD) + set(BREAKPAD "D:/bin/google-breakpad") + endif() if(CMAKE_BUILD_TYPE STREQUAL "Release") list(APPEND LIBS "${BREAKPAD}/src/client/windows/Release/lib/common.lib" @@ -90,24 +106,47 @@ if(USE_BREAKPAD) endif() elseif(UNIX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive") - set(BREAKPAD "~/Programmation/google-breakpad") + if(NOT DEFINED BREAKPAD) + set(BREAKPAD "~/Programmation/google-breakpad") + endif() list(APPEND LIBS "${BREAKPAD}/src/client/linux/libbreakpad_client.a") endif() + message(STATUS "Using Google Breakpad from ${BREAKPAD}") include_directories(${BREAKPAD}/src) endif() -if (WIN32) +if(ANDROID) + add_library(${PROJECT_NAME} SHARED ${SOURCES} ${FORMS} ${UTILS_SOURCES}) +elseif(WIN32) add_executable(${PROJECT_NAME} WIN32 ${SOURCES} ${FORMS} ${UTILS_SOURCES}) else() add_executable(${PROJECT_NAME} ${SOURCES} ${FORMS} ${UTILS_SOURCES}) endif() target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES} ${LIBS} lib) -set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "Grabber") -install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) +set_target_properties(${PROJECT_NAME} PROPERTIES VERSION ${VERSION}) + +if(ANDROID) + # SSL + if(USE_SSL) + message(STATUS "Compiling with SSL support") + set(OPENSSL_ANDROID "D:/Programmation/C++/Qt/OpenSSH-android/openssl-1.0.2p") + message(STATUS "OpenSSL for Android dir: ${OPENSSL_ANDROID}") + list(APPEND APK_LIBS "${OPENSSL_ANDROID}/libcrypto.so" "${OPENSSL_ANDROID}/libssl.so") + endif() + + # Generate manifest file + set(ANDROID_PACKAGE_SOURCES "${CMAKE_CURRENT_LIST_DIR}/android") + configure_file("${ANDROID_PACKAGE_SOURCES}/AndroidManifest.xml.in" "${ANDROID_PACKAGE_SOURCES}/AndroidManifest.xml" @ONLY) + + include("qt-android-cmake/AddQtAndroidApk") + add_qt_android_apk("${PROJECT_NAME}_apk" ${PROJECT_NAME} BUILDTOOLS_REVISION "28.0.3" DEPENDS ${APK_LIBS} PACKAGE_SOURCES ${ANDROID_PACKAGE_SOURCES}) +else() + set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "Grabber") + install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) +endif() # Pre-compiled header if(USE_PCH) - include(PrecompiledHeader) - add_precompiled_header(${PROJECT_NAME} "src/pch.h" FORCEINCLUDE SOURCE_CXX "${CMAKE_CURRENT_SOURCE_DIR}/src/pch.cpp") + cotire(${PROJECT_NAME}) endif() diff --git a/gui/android/AndroidManifest.xml.in b/gui/android/AndroidManifest.xml.in new file mode 100644 index 000000000..58c2b1657 --- /dev/null +++ b/gui/android/AndroidManifest.xml.in @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/android/res/drawable-hdpi/icon.png b/gui/android/res/drawable-hdpi/icon.png new file mode 100644 index 000000000..85ded6848 Binary files /dev/null and b/gui/android/res/drawable-hdpi/icon.png differ diff --git a/gui/android/res/drawable-mdpi/icon.png b/gui/android/res/drawable-mdpi/icon.png new file mode 100644 index 000000000..a1c9d6ff8 Binary files /dev/null and b/gui/android/res/drawable-mdpi/icon.png differ diff --git a/gui/android/res/drawable-xhdpi/icon.png b/gui/android/res/drawable-xhdpi/icon.png new file mode 100644 index 000000000..f66896df5 Binary files /dev/null and b/gui/android/res/drawable-xhdpi/icon.png differ diff --git a/gui/resources/android/AndroidManifest.xml b/gui/resources/android/AndroidManifest.xml new file mode 100644 index 000000000..75a725d4b --- /dev/null +++ b/gui/resources/android/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/src/about-window.cpp b/gui/src/about-window.cpp index 473688d67..77b20850f 100644 --- a/gui/src/about-window.cpp +++ b/gui/src/about-window.cpp @@ -12,10 +12,11 @@ AboutWindow::AboutWindow(const QString &version, QWidget *parent) QString labelVersion = version; #ifdef NIGHTLY QString commit(NIGHTLY_COMMIT); - if (!commit.isEmpty()) + if (!commit.isEmpty()) { labelVersion += QString(" - nightly (%1)").arg(commit.left(8)); - else + } else { labelVersion += " - nightly"; + } #endif ui->labelCurrent->setText(labelVersion); diff --git a/gui/src/android.cpp b/gui/src/android.cpp new file mode 100644 index 000000000..a73f8f467 --- /dev/null +++ b/gui/src/android.cpp @@ -0,0 +1,21 @@ +#include +#if defined(Q_OS_ANDROID) + +#include "android.h" +#include + + +bool checkPermission(const QString &perm) +{ + auto already = QtAndroid::checkPermission(perm); + if (already == QtAndroid::PermissionResult::Denied) { + QtAndroid::requestPermissionsSync(QStringList() << perm); + auto result = QtAndroid::checkPermission(perm); + if (result == QtAndroid::PermissionResult::Denied) { + return false; + } + } + return true; +} + +#endif // defined(Q_OS_ANDROID) diff --git a/gui/src/android.h b/gui/src/android.h new file mode 100644 index 000000000..7df17f07a --- /dev/null +++ b/gui/src/android.h @@ -0,0 +1,9 @@ +#ifndef ANDROID_H +#define ANDROID_H + +#include + + +bool checkPermission(const QString &perm); + +#endif // ANDROID_H diff --git a/gui/src/batch-download-image.cpp b/gui/src/batch-download-image.cpp index 6ab5442be..aa44547af 100644 --- a/gui/src/batch-download-image.cpp +++ b/gui/src/batch-download-image.cpp @@ -5,19 +5,20 @@ const DownloadQuery *BatchDownloadImage::query() const { - if (queryGroup != nullptr) + if (queryGroup != nullptr) { return queryGroup; + } return queryImage; } int BatchDownloadImage::siteId(const QList &groups) const { - if (queryGroup != nullptr) - { + if (queryGroup != nullptr) { const int index = groups.indexOf(*queryGroup); - if (index >= 0) + if (index >= 0) { return index + 1; + } } return -1; diff --git a/gui/src/batch/add-group-window.cpp b/gui/src/batch/add-group-window.cpp index 27449f911..24f40dab6 100644 --- a/gui/src/batch/add-group-window.cpp +++ b/gui/src/batch/add-group-window.cpp @@ -17,19 +17,18 @@ AddGroupWindow::AddGroupWindow(Site *selected, Profile *profile, QWidget *parent ui->comboSites->addItems(keys); ui->comboSites->setCurrentIndex(keys.indexOf(selected->url())); - QStringList completion; - completion.append(profile->getAutoComplete()); - completion.append(profile->getCustomAutoComplete()); - auto *completer = new QCompleter(completion, this); + auto *completer = new QCompleter(profile->getAutoComplete(), this); completer->setCaseSensitivity(Qt::CaseInsensitive); m_lineTags = new TextEdit(profile, this); m_lineTags->setCompleter(completer); ui->formLayout->setWidget(1, QFormLayout::FieldRole, m_lineTags); + setTabOrder(ui->comboSites, m_lineTags); m_linePostFiltering = new TextEdit(profile, this); m_linePostFiltering->setCompleter(completer); ui->formLayout->setWidget(5, QFormLayout::FieldRole, m_linePostFiltering); + setTabOrder(ui->spinLimit, m_linePostFiltering); } /** @@ -37,8 +36,9 @@ AddGroupWindow::AddGroupWindow(Site *selected, Profile *profile, QWidget *parent */ void AddGroupWindow::ok() { + const QStringList tags = m_lineTags->toPlainText().split(' ', QString::SkipEmptyParts); const QStringList postFiltering = m_linePostFiltering->toPlainText().split(' ', QString::SkipEmptyParts); Site *site = m_sites.value(ui->comboSites->currentText()); - emit sendData(DownloadQueryGroup(m_lineTags->toPlainText(), ui->spinPage->value(), ui->spinPP->value(), ui->spinLimit->value(), postFiltering, ui->checkBlacklist->isChecked(), site, m_settings->value("Save/filename").toString(), m_settings->value("Save/path").toString())); + emit sendData(DownloadQueryGroup(tags, ui->spinPage->value(), ui->spinPP->value(), ui->spinLimit->value(), postFiltering, ui->checkBlacklist->isChecked(), site, m_settings->value("Save/filename").toString(), m_settings->value("Save/path").toString())); close(); } diff --git a/gui/src/batch/add-unique-window.cpp b/gui/src/batch/add-unique-window.cpp index 429d8c453..25e9f7eb8 100644 --- a/gui/src/batch/add-unique-window.cpp +++ b/gui/src/batch/add-unique-window.cpp @@ -1,5 +1,7 @@ #include "batch/add-unique-window.h" #include +#include +#include #include #include #include "downloader/download-query-image.h" @@ -24,6 +26,49 @@ AddUniqueWindow::AddUniqueWindow(Site *selected, Profile *profile, QWidget *pare QSettings *settings = profile->getSettings(); ui->lineFolder->setText(settings->value("Save/path").toString()); ui->lineFilename->setText(settings->value("Save/filename").toString()); + + ui->lineId->setContentsMargins(0, 0, 0, 0); + ui->lineId->document()->setDocumentMargin(3); + ui->lineMd5->setContentsMargins(0, 0, 0, 0); + ui->lineMd5->document()->setDocumentMargin(3); + toggleMultiLineId(false); + toggleMultiLineMd5(false); + ui->progressBar->hide(); +} + +void setTextEditRows(QPlainTextEdit *ptxt, int nRows) +{ + const QTextDocument *pdoc = ptxt->document(); + const QFontMetrics fm(pdoc->defaultFont()); + const QMargins margins = ptxt->contentsMargins(); + + const int nHeight = fm.lineSpacing() * nRows + + qRound((pdoc->documentMargin() + ptxt->frameWidth()) * 2) + + margins.top() + + margins.bottom(); + ptxt->setFixedHeight(nHeight); +} +void AddUniqueWindow::toggleMultiLine(bool toggle, QPlainTextEdit *ptxt, QLabel *label) +{ + if (toggle) { + setTextEditRows(ptxt, 6); + } else { + setTextEditRows(ptxt, 1); + } + + ptxt->verticalScrollBar()->setVisible(toggle); + label->setVisible(toggle); + + update(); + resize(width(), 0); +} +void AddUniqueWindow::toggleMultiLineId(bool toggle) +{ + toggleMultiLine(toggle, ui->lineId, ui->labelLineId); +} +void AddUniqueWindow::toggleMultiLineMd5(bool toggle) +{ + toggleMultiLine(toggle, ui->lineMd5, ui->labelLineMd5); } /** @@ -32,8 +77,9 @@ AddUniqueWindow::AddUniqueWindow(Site *selected, Profile *profile, QWidget *pare void AddUniqueWindow::on_buttonFolder_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Choose a save folder"), ui->lineFolder->text()); - if (!folder.isEmpty()) - { ui->lineFolder->setText(folder); } + if (!folder.isEmpty()) { + ui->lineFolder->setText(folder); + } } void AddUniqueWindow::on_lineFilename_textChanged(const QString &text) { @@ -54,24 +100,61 @@ void AddUniqueWindow::ok(bool close) m_close = close; Api *api = site->detailsApi(); - if (api != nullptr) - { - const QString url = api->detailsUrl(ui->lineId->text().toULongLong(), ui->lineMd5->text(), site).url; + + const QStringList ids = ui->lineId->toPlainText().split('\n', QString::SkipEmptyParts); + for (const QString &id : ids) { + UniqueQuery q; + q.site = site; + q.api = api; + q.id = id.trimmed(); + m_queue.enqueue(q); + } + + const QStringList md5s = ui->lineMd5->toPlainText().split('\n', QString::SkipEmptyParts); + for (const QString &md5 : md5s) { + UniqueQuery q; + q.site = site; + q.api = api; + q.md5 = md5.trimmed(); + m_queue.enqueue(q); + } + + if (m_queue.count() > 1) { + ui->progressBar->setMaximum(m_queue.count()); + ui->progressBar->setValue(0); + ui->progressBar->show(); + } + + loadNext(); +} +void AddUniqueWindow::loadNext() +{ + if (m_queue.isEmpty()) { + if (m_close) { + close(); + } else { + ui->progressBar->hide(); + } + return; + } + + const UniqueQuery q = m_queue.dequeue(); + + if (q.api != nullptr && false) { + const QString url = q.api->detailsUrl(q.id.toULongLong(), q.md5, q.site).url; auto details = QMap(); details.insert("page_url", url); - details.insert("id", ui->lineId->text()); - details.insert("md5", ui->lineMd5->text()); + details.insert("id", q.id); + details.insert("md5", q.md5); - m_image = QSharedPointer(new Image(site, details, m_profile)); + m_image = QSharedPointer(new Image(q.site, details, m_profile)); connect(m_image.data(), &Image::finishedLoadingTags, this, &AddUniqueWindow::addLoadedImage); m_image->loadDetails(); - } - else - { - const QString query = (ui->lineId->text().isEmpty() ? "md5:" + ui->lineMd5->text() : "id:" + ui->lineId->text()); + } else { + const QString query = (q.id.isEmpty() ? "md5:" + q.md5 : "id:" + q.id); const QStringList search = QStringList() << query << "status:any"; - m_page = new Page(m_profile, site, m_sites.values(), search, 1, 1); + m_page = new Page(m_profile, q.site, m_sites.values(), search, 1, 1); connect(m_page, &Page::finishedLoading, this, &AddUniqueWindow::replyFinished); m_page->load(); } @@ -79,8 +162,7 @@ void AddUniqueWindow::ok(bool close) void AddUniqueWindow::replyFinished(Page *p) { - if (p->images().isEmpty()) - { + if (p->images().isEmpty()) { p->deleteLater(); error(this, tr("No image found.")); return; @@ -93,11 +175,13 @@ void AddUniqueWindow::replyFinished(Page *p) void AddUniqueWindow::addLoadedImage() { addImage(m_image); + // m_image->deleteLater(); } void AddUniqueWindow::addImage(const QSharedPointer &img) { - emit sendData(DownloadQueryImage(*img, m_sites[ui->comboSites->currentText()], ui->lineFilename->text(), ui->lineFolder->text())); + emit sendData(DownloadQueryImage(img, m_sites[ui->comboSites->currentText()], ui->lineFilename->text(), ui->lineFolder->text())); + + ui->progressBar->setValue(ui->progressBar->value() + 1); - if (m_close) - close(); + loadNext(); } diff --git a/gui/src/batch/add-unique-window.h b/gui/src/batch/add-unique-window.h index 702090cea..0cefe9b73 100644 --- a/gui/src/batch/add-unique-window.h +++ b/gui/src/batch/add-unique-window.h @@ -3,6 +3,7 @@ #include #include +#include #include @@ -12,18 +13,30 @@ namespace Ui } +class Api; class Site; class Profile; +class QLabel; +class QPlainTextEdit; class Image; class Page; class DownloadQueryImage; +struct UniqueQuery +{ + Site *site; + Api *api; + QString id; + QString md5; +}; + class AddUniqueWindow : public QDialog { Q_OBJECT public: AddUniqueWindow(Site *selected, Profile *profile, QWidget *parent = nullptr); + void loadNext(); public slots: void add(); @@ -33,14 +46,20 @@ class AddUniqueWindow : public QDialog void addImage(const QSharedPointer &img); void on_buttonFolder_clicked(); void on_lineFilename_textChanged(const QString &); + void toggleMultiLineId(bool toggle); + void toggleMultiLineMd5(bool toggle); signals: void sendData(const DownloadQueryImage &); + protected: + void toggleMultiLine(bool toggle, QPlainTextEdit *ptxt, QLabel *label); + private: Ui::AddUniqueWindow *ui; Page *m_page; QMap m_sites; + QQueue m_queue; bool m_close; Profile *m_profile; QSharedPointer m_image; diff --git a/gui/src/batch/add-unique-window.ui b/gui/src/batch/add-unique-window.ui index ebee527c0..8483d700a 100644 --- a/gui/src/batch/add-unique-window.ui +++ b/gui/src/batch/add-unique-window.ui @@ -7,7 +7,7 @@ 0 0 300 - 209 + 384 @@ -17,27 +17,6 @@ QFormLayout::AllNonFixedFieldsGrow - - - - - - Add - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - @@ -45,6 +24,9 @@ + + + @@ -59,32 +41,6 @@ - - - - - - - - - - - - - Filename - - - - - - - - - - - - - @@ -106,6 +62,23 @@ + + + + Filename + + + + + + + + + + + + + @@ -119,8 +92,153 @@ + + + + + + Add + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + QPlainTextEdit::NoWrap + + + + + + + <i>One ID per line.</i> + + + + + + + + + + + + 30 + 16777215 + + + + + + + + true + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + + + + QPlainTextEdit::NoWrap + + + + + + + <i>One MD5 per line.</i> + + + + + + + + + + + + 30 + 0 + + + + + + + + true + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + + comboSites + lineId + buttonLineId + lineMd5 + buttonLineMd5 + lineFolder + buttonFolder + lineFilename + pushButton + @@ -130,8 +248,8 @@ reject() - 285 - 207 + 289 + 346 286 @@ -146,8 +264,8 @@ ok() - 171 - 190 + 262 + 346 169 @@ -162,8 +280,8 @@ add() - 70 - 204 + 80 + 346 69 @@ -172,66 +290,66 @@ - lineId + lineFilename returnPressed() AddUniqueWindow ok() - 160 - 41 + 290 + 285 289 - 45 + 132 - lineMd5 + lineFolder returnPressed() AddUniqueWindow ok() - 257 - 76 + 208 + 256 - 288 - 76 + 292 + 105 - lineFilename - returnPressed() + buttonLineId + toggled(bool) AddUniqueWindow - ok() + toggleMultiLineId(bool) - 281 - 133 + 277 + 47 - 289 - 132 + 294 + 61 - lineFolder - returnPressed() + buttonLineMd5 + toggled(bool) AddUniqueWindow - ok() + toggleMultiLineMd5(bool) - 188 - 100 + 279 + 151 - 292 - 105 + 252 + 119 @@ -239,5 +357,7 @@ ok() add() + toggleMultiLineId(bool) + toggleMultiLineMd5(bool) diff --git a/gui/src/batch/batch-window.cpp b/gui/src/batch/batch-window.cpp index 6f4f70d0f..36509e335 100644 --- a/gui/src/batch/batch-window.cpp +++ b/gui/src/batch/batch-window.cpp @@ -10,9 +10,12 @@ #include "functions.h" #include "loader/downloadable.h" +#define SPEED_SMOOTHING_IMAGE 0.3 +#define SPEED_SMOOTHING_AVERAGE 0.3 + BatchWindow::BatchWindow(QSettings *settings, QWidget *parent) - : QDialog(parent), ui(new Ui::BatchWindow), m_settings(settings), m_imagesCount(0), m_items(0), m_images(0), m_maxSpeeds(0), m_lastDownloading(0), m_cancel(false), m_paused(false) + : QDialog(parent), ui(new Ui::BatchWindow), m_settings(settings), m_imagesCount(0), m_items(0), m_images(0), m_maxSpeeds(0), m_lastDownloading(0), m_mean(SPEED_SMOOTHING_AVERAGE), m_cancel(false), m_paused(false) { ui->setupUi(this); ui->tableWidget->resizeColumnToContents(0); @@ -62,13 +65,12 @@ void BatchWindow::closeEvent(QCloseEvent *e) m_settings->setValue("Batch/scrollToDownload", ui->checkScrollToDownload->isChecked()); m_settings->sync(); - if (m_images < m_imagesCount || m_imagesCount == -1) - { + if (m_images < m_imagesCount || m_imagesCount == -1) { cancel(); emit rejected(); + } else { + clear(); } - else - { clear(); } #ifdef Q_OS_WIN m_taskBarProgress->setVisible(false); @@ -83,6 +85,12 @@ void BatchWindow::pause() ui->labelSpeed->setText(m_paused ? tr("Paused") : QString()); ui->buttonPause->setText(m_paused ? tr("Resume") : tr("Pause")); + // Reset download speeds + m_mean.clear(); + for (auto it = m_speeds.begin(); it != m_speeds.end(); ++it) { + it.value().clear(); + } + #ifdef Q_OS_WIN m_taskBarProgress->setPaused(m_paused); #endif @@ -154,19 +162,17 @@ void BatchWindow::copyToClipboard() QList selected = ui->tableWidget->selectedItems(); int count = selected.size(); QStringList urls = QStringList(); - if (count < 1) - { + if (count < 1) { count = ui->tableWidget->rowCount(); urls.reserve(count); - for (int i = 0; i < count; i++) - { urls.append(ui->tableWidget->item(i, 2)->text()); } - } - else - { - for (int i = 0; i < count; i++) - { - if (selected.at(i)->icon().isNull()) - { urls.append(selected.at(i)->text()); } + for (int i = 0; i < count; i++) { + urls.append(ui->tableWidget->item(i, 2)->text()); + } + } else { + for (int i = 0; i < count; i++) { + if (selected.at(i)->icon().isNull()) { + urls.append(selected.at(i)->text()); + } } } qApp->clipboard()->setText(urls.join('\n')); @@ -176,7 +182,11 @@ void BatchWindow::setCount(int cnt) { ui->tableWidget->setRowCount(cnt); } void BatchWindow::addImage(const QUrl &url, int batch, double size) { - m_urls.insert(url, m_items); + if (m_urls.contains(url)) { + m_urls[url].append(m_items); + } else { + m_urls.insert(url, {m_items}); + } static QIcon pendingIcon(":/images/status/pending.png"); QTableWidgetItem *id = new QTableWidgetItem(QString::number(m_items + 1)); @@ -213,29 +223,33 @@ void BatchWindow::updateColumns() } int BatchWindow::indexOf(const QUrl &url) { - const int i = m_urls[url]; - if (i < 0 || ui->tableWidget->item(i, 1) == nullptr) + const auto vals = m_urls.value(url); + const int i = vals.isEmpty() ? -1 : vals.first(); + if (i < 0 || ui->tableWidget->item(i, 1) == nullptr) { return -1; + } return i; } int BatchWindow::batch(const QUrl &url) { const int i = indexOf(url); - if (i == -1) + if (i == -1) { return -1; + } return ui->tableWidget->item(i, 1)->text().toInt(); } void BatchWindow::loadingImage(const QUrl &url) { - if (m_start->isNull()) + if (m_start->isNull()) { m_start->start(); - m_speeds.insert(url, 0); - if (m_speeds.size() > m_maxSpeeds) + } + m_speeds.insert(url, ExponentialMovingAverage(SPEED_SMOOTHING_IMAGE)); + if (m_speeds.size() > m_maxSpeeds) { m_maxSpeeds = m_speeds.size(); + } const int i = indexOf(url); - if (i != -1) - { + if (i != -1) { static QIcon downloadingIcon(":/images/status/downloading.png"); ui->tableWidget->item(i, 0)->setIcon(downloadingIcon); scrollTo(i); @@ -244,8 +258,7 @@ void BatchWindow::loadingImage(const QUrl &url) void BatchWindow::scrollTo(int i) { // Go to downloading image - if (ui->checkScrollToDownload->isChecked() && i >= m_lastDownloading) - { + if (ui->checkScrollToDownload->isChecked() && i >= m_lastDownloading) { ui->tableWidget->scrollToItem(ui->tableWidget->item(i, 0)); m_lastDownloading = i; } @@ -253,10 +266,14 @@ void BatchWindow::scrollTo(int i) void BatchWindow::imageUrlChanged(const QUrl &before, const QUrl &after) { const int i = indexOf(before); - if (i != -1) - { - m_urls.remove(before); - m_urls.insert(after, i); + if (i != -1) { + const auto vals = m_urls.value(before); + if (vals.count() == 1) { + m_urls.remove(before); + } else { + m_urls[before].removeFirst(); + } + m_urls.insert(after, {i}); ui->tableWidget->item(i, 2)->setText(after.toString()); ui->tableWidget->item(i, 3)->setText(QString()); ui->tableWidget->item(i, 4)->setText(QString()); @@ -266,25 +283,28 @@ void BatchWindow::imageUrlChanged(const QUrl &before, const QUrl &after) void BatchWindow::statusImage(const QUrl &url, int percent) { const int i = indexOf(url); - if (i != -1) + if (i != -1) { ui->tableWidget->item(i, 5)->setText(QString::number(percent) + " %"); + } } void BatchWindow::speedImage(const QUrl &url, double speed) { - m_speeds[url] = static_cast(speed); - const QString unit = getUnit(&speed) + "/s"; + m_speeds[url].addValue(speed); + + double average = m_speeds[url].average(); + const QString unit = getUnit(&average) + "/s"; int i = indexOf(url); - if (i != -1) - ui->tableWidget->item(i, 4)->setText(QLocale::system().toString(speed, 'f', speed < 10 ? 2 : 0) + " " + unit); + if (i != -1) { + ui->tableWidget->item(i, 4)->setText(QLocale::system().toString(average, 'f', average < 10 ? 2 : 0) + " " + unit); + } drawSpeed(); } void BatchWindow::sizeImage(const QUrl &url, double size) { int i = indexOf(url); - if (i != -1) - { + if (i != -1) { const QString unit = getUnit(&size); const QString label = size > 0 ? QLocale::system().toString(size, 'f', size < 10 ? 2 : 0) + " " + unit @@ -305,8 +325,7 @@ void BatchWindow::loadedImage(const QUrl &url, Downloadable::SaveResult result) // Update table const int i = indexOf(url); - if (i != -1) - { + if (i != -1) { scrollTo(i); ui->tableWidget->item(i, 4)->setText(QString()); ui->tableWidget->item(i, 5)->setText(QString()); @@ -342,25 +361,21 @@ void BatchWindow::loadedImage(const QUrl &url, Downloadable::SaveResult result) void BatchWindow::drawSpeed() { - if (m_time->elapsed() < 1000) - { return; } + if (m_time->elapsed() < 1000) { + return; + } m_time->restart(); double speed = 0; - for (auto sp = m_speeds.constBegin(); sp != m_speeds.constEnd(); ++sp) - { speed += sp.value(); } - if (m_speeds.size() == m_maxSpeeds) - { m_mean.append(qRound(speed)); } + for (auto sp = m_speeds.constBegin(); sp != m_speeds.constEnd(); ++sp) { + speed += sp.value().average(); + } + if (m_speeds.size() == m_maxSpeeds) { + m_mean.addValue(speed); + } const QString unit = getUnit(&speed) + "/s"; - double speedMean = 0; - const int count = qMin(m_mean.count(), 60); - if (count > 0) - { - for (int i = m_mean.count() - count; i < m_mean.count() - 1; i++) - { speedMean += m_mean[i]; } - speedMean = static_cast(speedMean / count); - } + double speedMean = m_mean.average(); const QString unitMean = getUnit(&speedMean) + "/s"; const int elapsed = m_start->elapsed(); @@ -377,13 +392,10 @@ void BatchWindow::drawSpeed() void BatchWindow::on_buttonDetails_clicked(bool visible) { - if (ui->details->isHidden() || visible) - { + if (ui->details->isHidden() || visible) { ui->details->show(); resize(m_currentSize); - } - else - { + } else { ui->details->hide(); m_currentSize = size(); resize(QSize(300, 0)); @@ -404,8 +416,9 @@ void BatchWindow::setTotalValue(int val) ui->labelImages->setText(QStringLiteral("%1/%2").arg(m_images).arg(m_imagesCount)); ui->progressTotal->setValue(val); - if (val >= m_imagesCount) - { ui->cancelButton->setText(tr("Close")); } + if (val >= m_imagesCount) { + ui->cancelButton->setText(tr("Close")); + } #ifdef Q_OS_WIN m_taskBarProgress->setValue(val); diff --git a/gui/src/batch/batch-window.h b/gui/src/batch/batch-window.h index c8bea2050..0b5431620 100644 --- a/gui/src/batch/batch-window.h +++ b/gui/src/batch/batch-window.h @@ -5,6 +5,7 @@ #include #include #include +#include "exponential-moving-average.h" #include "loader/downloadable.h" @@ -76,10 +77,10 @@ class BatchWindow : public QDialog QSettings *m_settings; QSize m_currentSize; int m_imagesCount, m_items, m_images, m_maxSpeeds, m_lastDownloading; - QMap m_urls; + QMap> m_urls; QList m_progressBars; - QMap m_speeds; - QList m_mean; + QMap m_speeds; + ExponentialMovingAverage m_mean; bool m_cancel, m_paused; QTime *m_time, *m_start; #ifdef Q_OS_WIN diff --git a/gui/src/crashhandler/crashhandler.cpp b/gui/src/crashhandler/crashhandler.cpp index be88ae19a..6618215ab 100644 --- a/gui/src/crashhandler/crashhandler.cpp +++ b/gui/src/crashhandler/crashhandler.cpp @@ -7,6 +7,7 @@ #include #include #include "functions.h" +#include "logger.h" #if defined(Q_OS_MAC) #include "client/mac/handler/exception_handler.h" #elif defined(Q_OS_LINUX) diff --git a/gui/src/favorite-window.cpp b/gui/src/favorite-window.cpp index fa6de501d..cdada8471 100644 --- a/gui/src/favorite-window.cpp +++ b/gui/src/favorite-window.cpp @@ -28,8 +28,7 @@ FavoriteWindow::FavoriteWindow(Profile *profile, Favorite favorite, QWidget *par QStringList sourceKeys = profile->getSites().keys(); ui->comboMonitoringSource->addItems(sourceKeys); - if (!m_favorite.getMonitors().isEmpty()) - { + if (!m_favorite.getMonitors().isEmpty()) { Monitor monitor = m_favorite.getMonitors().first(); ui->spinMonitoringInterval->setValue(qFloor(monitor.interval() / 60.0)); ui->comboMonitoringSource->setCurrentIndex(sourceKeys.indexOf(monitor.site()->url())); @@ -61,8 +60,9 @@ void FavoriteWindow::on_buttonRemove_clicked() void FavoriteWindow::on_openButton_clicked() { QString file = QFileDialog::getOpenFileName(this, tr("Choose an image"), m_profile->getSettings()->value("Save/path").toString(), "Images (*.png *.gif *.jpg *.jpeg)"); - if (!file.isEmpty()) - { ui->imageLineEdit->setText(file); } + if (!file.isEmpty()) { + ui->imageLineEdit->setText(file); + } } /** @@ -76,12 +76,11 @@ void FavoriteWindow::save() int interval = ui->spinMonitoringInterval->value() * 60; Site *site = m_profile->getSites().value(ui->comboMonitoringSource->currentText()); QList monitors = oldFav.getMonitors(); - if (interval == 0) - { monitors.clear(); } - else if (monitors.isEmpty()) - { monitors.append(Monitor(site, interval, QDateTime::currentDateTimeUtc())); } - else - { + if (interval == 0) { + monitors.clear(); + } else if (monitors.isEmpty()) { + monitors.append(Monitor(site, interval, QDateTime::currentDateTimeUtc())); + } else { Monitor rep(site, interval, monitors[0].lastCheck()); monitors[0] = rep; } @@ -89,18 +88,18 @@ void FavoriteWindow::save() m_favorite = Favorite(ui->tagLineEdit->text(), ui->noteSpinBox->value(), ui->lastViewedDateTimeEdit->dateTime(), monitors); m_favorite.setImagePath(savePath("thumbs/" + m_favorite.getName(true) + ".png")); - if (oldFav.getName() != m_favorite.getName()) - { - if (QFile::exists(savePath("thumbs/" + oldFav.getName(true) + ".png"))) - { QFile::rename(savePath("thumbs/" + oldFav.getName(true) + ".png"), m_favorite.getImagePath()); } + if (oldFav.getName() != m_favorite.getName()) { + if (QFile::exists(savePath("thumbs/" + oldFav.getName(true) + ".png"))) { + QFile::rename(savePath("thumbs/" + oldFav.getName(true) + ".png"), m_favorite.getImagePath()); + } m_profile->removeFavorite(oldFav); } - if (QFile::exists(ui->imageLineEdit->text())) - { + if (QFile::exists(ui->imageLineEdit->text())) { QPixmap img(ui->imageLineEdit->text()); - if (!img.isNull()) - { m_favorite.setImage(img); } + if (!img.isNull()) { + m_favorite.setImage(img); + } } m_profile->addFavorite(m_favorite); } diff --git a/gui/src/helpers.cpp b/gui/src/helpers.cpp index 42006eb5f..32d3218ff 100644 --- a/gui/src/helpers.cpp +++ b/gui/src/helpers.cpp @@ -42,13 +42,10 @@ void showInGraphicalShell(const QString &pathIn) ITEMIDLIST *pidl = nullptr; SFGAOF out; HRESULT hr = SHParseDisplayName(filename, nullptr, &pidl, SFGAO_FILESYSTEM, &out); - if (SUCCEEDED(hr)) - { + if (SUCCEEDED(hr)) { SHOpenFolderAndSelectItems(pidl, 0, nullptr, 0); ILFree(pidl); - } - else - { + } else { LPCTSTR errMsg = _com_error(hr).ErrorMessage(); QString msg = QString::fromLatin1(errMsg); log(QString("Error parsing path display name for '%1': %2").arg(pathIn, msg), Logger::Error); @@ -67,19 +64,19 @@ void showInGraphicalShell(const QString &pathIn) void clearLayout(QLayout *layout) { - if (layout == nullptr) + if (layout == nullptr) { return; + } - while (layout->count() > 0) - { + while (layout->count() > 0) { QLayoutItem *item = layout->takeAt(0); - if (item->layout() != nullptr) - { + if (item->layout() != nullptr) { clearLayout(item->layout()); item->layout()->deleteLater(); } - if (item->widget() != nullptr) - { item->widget()->deleteLater(); } + if (item->widget() != nullptr) { + item->widget()->deleteLater(); + } delete item; } } diff --git a/gui/src/image-context-menu.cpp b/gui/src/image-context-menu.cpp index a2fe96bde..6e085872f 100644 --- a/gui/src/image-context-menu.cpp +++ b/gui/src/image-context-menu.cpp @@ -21,8 +21,7 @@ ImageContextMenu::ImageContextMenu(QSettings *settings, QSharedPointer im QMenu *reverseSearchMenu = addMenu(QIcon(":/images/icons/globe.png"), tr("Web services")); auto *reverseSearchMapper = new QSignalMapper(this); connect(reverseSearchMapper, SIGNAL(mapped(int)), this, SLOT(reverseImageSearch(int))); - for (int i = 0; i < m_reverseSearchEngines.count(); ++i) - { + for (int i = 0; i < m_reverseSearchEngines.count(); ++i) { ReverseSearchEngine engine = m_reverseSearchEngines[i]; QAction *subMenuAct = reverseSearchMenu->addAction(engine.icon(), engine.name()); connect(subMenuAct, SIGNAL(triggered()), reverseSearchMapper, SLOT(map())); @@ -46,8 +45,9 @@ void ImageContextMenu::searchMd5() void ImageContextMenu::reverseImageSearch(int i) { - if (m_reverseSearchEngines.count() < i) + if (m_reverseSearchEngines.count() < i) { return; + } m_reverseSearchEngines[i].searchByUrl(m_image->fileUrl()); } diff --git a/gui/src/main-window.cpp b/gui/src/main-window.cpp index d828ec53c..33b6da5bd 100644 --- a/gui/src/main-window.cpp +++ b/gui/src/main-window.cpp @@ -21,6 +21,7 @@ #include "downloader/download-query-image.h" #include "functions.h" #include "helpers.h" +#include "logger.h" #include "models/api/api.h" #include "models/favorite.h" #include "models/filename.h" @@ -51,9 +52,11 @@ MainWindow::MainWindow(Profile *profile) : ui(new Ui::MainWindow), m_profile(profile), m_favorites(m_profile->getFavorites()), m_loaded(false), m_forcedTab(-1), m_languageLoader(savePath("languages/", true)), m_currentTab(nullptr) -{ } +{} void MainWindow::init(const QStringList &args, const QMap ¶ms) { + setAttribute(Qt::WA_DeleteOnClose); + m_settings = m_profile->getSettings(); auto sites = m_profile->getSites(); @@ -61,8 +64,7 @@ void MainWindow::init(const QStringList &args, const QMap &par themeLoader.setTheme(m_settings->value("theme", "Default").toString()); ui->setupUi(this); - if (m_settings->value("Log/show", true).toBool()) - { + if (m_settings->value("Log/show", true).toBool()) { m_logTab = new LogTab(this); ui->tabWidget->addTab(m_logTab, m_logTab->windowTitle()); } @@ -72,10 +74,11 @@ void MainWindow::init(const QStringList &args, const QMap &par log(QStringLiteral("Path: `%1`").arg(qApp->applicationDirPath()), Logger::Info); log(QStringLiteral("Loading preferences from `%1`").arg(m_settings->fileName()), Logger::Info); - if (!QSslSocket::supportsSsl()) - { log(QStringLiteral("Missing SSL libraries"), Logger::Error); } - else - { log(QStringLiteral("SSL libraries: %1").arg(QSslSocket::sslLibraryVersionString()), Logger::Info); } + if (!QSslSocket::supportsSsl()) { + log(QStringLiteral("Missing SSL libraries"), Logger::Error); + } else { + log(QStringLiteral("SSL libraries: %1").arg(QSslSocket::sslLibraryVersionString()), Logger::Info); + } bool crashed = m_settings->value("crashed", false).toBool(); m_settings->setValue("crashed", true); @@ -83,8 +86,7 @@ void MainWindow::init(const QStringList &args, const QMap &par // On first launch after setup, we restore the setup's language QString setupSettingsFile = savePath("innosetup.ini"); - if (QFile::exists(setupSettingsFile)) - { + if (QFile::exists(setupSettingsFile)) { QSettings setupSettings(setupSettingsFile, QSettings::IniFormat); QString setupLanguage = setupSettings.value("language", "en").toString(); @@ -93,8 +95,7 @@ void MainWindow::init(const QStringList &args, const QMap &par QStringList keys = associations.childKeys(); // Only if the setup language is available in Grabber - if (keys.contains(setupLanguage)) - { + if (keys.contains(setupLanguage)) { m_settings->setValue("language", associations.value(setupLanguage).toString()); } @@ -119,13 +120,11 @@ void MainWindow::init(const QStringList &args, const QMap &par m_favorites = m_profile->getFavorites(); - if (m_settings->value("Proxy/use", false).toBool()) - { + if (m_settings->value("Proxy/use", false).toBool()) { bool useSystem = m_settings->value("Proxy/useSystem", false).toBool(); QNetworkProxyFactory::setUseSystemConfiguration(useSystem); - if (!useSystem) - { + if (!useSystem) { const QNetworkProxy::ProxyType type = m_settings->value("Proxy/type", "http").toString() == "http" ? QNetworkProxy::HttpProxy : QNetworkProxy::Socks5Proxy; @@ -138,14 +137,13 @@ void MainWindow::init(const QStringList &args, const QMap &par ); QNetworkProxy::setApplicationProxy(proxy); log(QStringLiteral("Enabling application proxy on host \"%1\" and port %2.").arg(m_settings->value("Proxy/hostName").toString()).arg(m_settings->value("Proxy/port").toInt()), Logger::Info); + } else { + log(QStringLiteral("Enabling system-wide proxy."), Logger::Info); } - else - { log(QStringLiteral("Enabling system-wide proxy."), Logger::Info); } } log(QStringLiteral("Loading sources"), Logger::Debug); - if (sites.empty()) - { + if (sites.empty()) { QMessageBox::critical(this, tr("No source found"), tr("No source found. Do you have a configuration problem? Try to reinstall the program.")); qApp->quit(); this->deleteLater(); @@ -154,13 +152,13 @@ void MainWindow::init(const QStringList &args, const QMap &par QString srsc; QStringList keys = sites.keys(); - for (const QString &key : keys) - { srsc += (!srsc.isEmpty() ? ", " : "") + key + " (" + sites.value(key)->type() + ")"; } + for (const QString &key : keys) { + srsc += (!srsc.isEmpty() ? ", " : "") + key + " (" + sites.value(key)->type() + ")"; + } log(QStringLiteral("%1 source%2 found: %3").arg(sites.size()).arg(sites.size() > 1 ? "s" : "", srsc), Logger::Info); // System tray icon - if (m_settings->value("Monitoring/enableTray", false).toBool()) - { + if (m_settings->value("Monitoring/enableTray", false).toBool()) { auto quitAction = new QAction(tr("&Quit"), this); connect(quitAction, &QAction::triggered, this, &MainWindow::trayClose); @@ -174,9 +172,9 @@ void MainWindow::init(const QStringList &args, const QMap &par connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated); connect(m_trayIcon, &QSystemTrayIcon::messageClicked, this, &MainWindow::trayMessageClicked); + } else { + m_trayIcon = nullptr; } - else - { m_trayIcon = nullptr; } ui->actionClosetab->setShortcut(QKeySequence::Close); QShortcut *actionCloseTabW = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), this); @@ -198,16 +196,14 @@ void MainWindow::init(const QStringList &args, const QMap &par connect(ui->actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); // Action on first load - if (m_settings->value("firstload", true).toBool()) - { + if (m_settings->value("firstload", true).toBool()) { this->onFirstLoad(); m_settings->setValue("firstload", false); } // Crash restoration m_restore = m_settings->value("start", "none").toString() == "restore"; - if (crashed) - { + if (crashed) { log(QStringLiteral("It seems that Imgbrd-Grabber hasn't shut down properly last time."), Logger::Warning); QString msg = tr("It seems that the application was not properly closed for its last use. Do you want to restore your last session?"); @@ -233,8 +229,9 @@ void MainWindow::init(const QStringList &args, const QMap &par ui->tabWidget->setCurrentIndex(0); // Restore download lists - if (m_restore) - { m_downloadsTab->loadLinkList(m_profile->getPath() + "/restore.igl"); } + if (m_restore) { + m_downloadsTab->loadLinkList(m_profile->getPath() + "/restore.igl"); + } // Favorites tab m_favoritesTab = new FavoritesTab(m_profile, this); @@ -280,10 +277,10 @@ void MainWindow::init(const QStringList &args, const QMap &par // Get list of selected sources QStringList sav = m_settings->value("sites", "").toStringList(); - for (const QString &key : sav) - { - if (!sites.contains(key)) + for (const QString &key : sav) { + if (!sites.contains(key)) { continue; + } Site *site = sites.value(key); connect(site, &Site::loggedIn, this, &MainWindow::initialLoginsFinished); @@ -292,23 +289,21 @@ void MainWindow::init(const QStringList &args, const QMap &par // Initial login on selected sources m_waitForLogin = 0; - if (m_selectedSites.isEmpty()) - { + if (m_selectedSites.isEmpty()) { initialLoginsDone(); - } - else - { + } else { m_waitForLogin += m_selectedSites.count(); - for (Site *site : qAsConst(m_selectedSites)) + for (Site *site : qAsConst(m_selectedSites)) { site->login(); + } } on_buttonInitSettings_clicked(); m_lineFolder_completer = QStringList(m_settings->value("Save/path").toString()); ui->lineFolder->setCompleter(new QCompleter(m_lineFolder_completer, ui->lineFolder)); - //m_lineFilename_completer = QStringList(m_settings->value("Save/filename").toString()); - //ui->lineFilename->setCompleter(new QCompleter(m_lineFilename_completer)); + // m_lineFilename_completer = QStringList(m_settings->value("Save/filename").toString()); + // ui->lineFilename->setCompleter(new QCompleter(m_lineFilename_completer)); ui->comboFilename->setAutoCompletionCaseSensitivity(Qt::CaseSensitive); connect(m_profile, &Profile::favoritesChanged, this, &MainWindow::updateFavorites); @@ -323,12 +318,10 @@ void MainWindow::parseArgs(const QStringList &args, const QMap { // When we use Grabber to open a file QStringList tags; - if (args.count() == 1 && QFile::exists(args[0])) - { + if (args.count() == 1 && QFile::exists(args[0])) { // Load an IGL file QFileInfo info(args[0]); - if (info.suffix() == QLatin1String("igl")) - { + if (info.suffix() == QLatin1String("igl")) { m_downloadsTab->loadLinkList(info.absoluteFilePath()); m_forcedTab = m_tabs.size() + 1; return; @@ -342,8 +335,7 @@ void MainWindow::parseArgs(const QStringList &args, const QMap // Other positional arguments are treated as tags tags.append(args); tags.append(params.value("tags").split(' ', QString::SkipEmptyParts)); - if (!tags.isEmpty() || m_settings->value("start", "none").toString() == "firstpage") - { + if (!tags.isEmpty() || m_settings->value("start", "none").toString() == "firstpage") { loadTag(tags.join(' '), true, false, false); } } @@ -354,18 +346,24 @@ void MainWindow::initialLoginsFinished() disconnect(site, &Site::loggedIn, this, &MainWindow::initialLoginsFinished); m_waitForLogin--; - if (m_waitForLogin != 0) + if (m_waitForLogin != 0) { return; + } initialLoginsDone(); } void MainWindow::initialLoginsDone() { - if (m_restore) - { loadTabs(m_profile->getPath() + "/tabs.txt"); } - if (m_tabs.isEmpty()) - { addTab(); } + if (m_restore) { + if (QFile::exists(m_profile->getPath() + "/tabs.txt")) { + QFile::rename(m_profile->getPath() + "/tabs.txt", m_profile->getPath() + "/tabs.json"); + } + loadTabs(m_profile->getPath() + "/tabs.json"); + } + if (m_tabs.isEmpty()) { + addTab(); + } m_currentTab = qobject_cast(ui->tabWidget->currentWidget()); m_loaded = true; @@ -379,15 +377,16 @@ void MainWindow::initialLoginsDone() MainWindow::~MainWindow() { - delete m_profile; + m_profile->deleteLater(); + delete ui; + ui = nullptr; } void MainWindow::focusSearch() { auto *tab = dynamic_cast(ui->tabWidget->currentWidget()); - if (tab != nullptr) - { + if (tab != nullptr) { tab->focusSearch(); } } @@ -401,11 +400,9 @@ void MainWindow::onFirstLoad() // Detect and Danbooru Downloader settings DanbooruDownloaderImporter ddImporter; - if (ddImporter.isInstalled()) - { + if (ddImporter.isInstalled()) { int reponse = QMessageBox::question(this, "", tr("The Mozilla Firefox addon \"Danbooru Downloader\" has been detected on your system. Do you want to load its preferences?"), QMessageBox::Yes | QMessageBox::No); - if (reponse == QMessageBox::Yes) - { + if (reponse == QMessageBox::Yes) { ddImporter.import(m_settings); return; } @@ -419,41 +416,52 @@ void MainWindow::onFirstLoad() swin->show(); } -void MainWindow::addTab(const QString &tag, bool background, bool save) +void MainWindow::addTab(const QString &tag, bool background, bool save, SearchTab *source) { auto *w = new TagTab(m_profile, this); - this->addSearchTab(w, background, save); + this->addSearchTab(w, background, save, source); - if (!tag.isEmpty()) - { w->setTags(tag); } - else - { w->focusSearch(); } + if (!tag.isEmpty()) { + w->setTags(tag); + } else { + w->focusSearch(); + } } -void MainWindow::addPoolTab(int pool, const QString &site, bool background, bool save) +void MainWindow::addPoolTab(int pool, const QString &site, bool background, bool save, SearchTab *source) { auto *w = new PoolTab(m_profile, this); - this->addSearchTab(w, background, save); - - if (!site.isEmpty()) - { w->setSite(site); } - if (pool != 0) - { w->setPool(pool, site); } - else - { w->focusSearch(); } + this->addSearchTab(w, background, save, source); + + if (!site.isEmpty()) { + w->setSite(site); + } + if (pool != 0) { + w->setPool(pool, site); + } else { + w->focusSearch(); + } } -void MainWindow::addGalleryTab(Site *site, QString name, QString id, bool background, bool save) +void MainWindow::addGalleryTab(Site *site, QSharedPointer gallery, bool background, bool save, SearchTab *source) { - auto *w = new GalleryTab(site, std::move(name), std::move(id), m_profile, this); - this->addSearchTab(w, background, save); + auto *w = new GalleryTab(site, std::move(gallery), m_profile, this); + this->addSearchTab(w, background, save, source); } -void MainWindow::addSearchTab(SearchTab *w, bool background, bool save) +void MainWindow::addSearchTab(SearchTab *w, bool background, bool save, SearchTab *source) { - if (m_tabs.size() > ui->tabWidget->currentIndex()) - { - w->setSources(m_tabs[ui->tabWidget->currentIndex()]->sources()); - w->setImagesPerPage(m_tabs[ui->tabWidget->currentIndex()]->imagesPerPage()); - w->setColumns(m_tabs[ui->tabWidget->currentIndex()]->columns()); - w->setPostFilter(m_tabs[ui->tabWidget->currentIndex()]->postFilter()); + // TODO(Bionus): remove this and always pass it when necessary + if (source == nullptr || !m_tabs.contains(source)) { + if (m_tabs.size() > ui->tabWidget->currentIndex()) { + source = m_tabs[ui->tabWidget->currentIndex()]; + } else { + source = nullptr; + } + } + + if (source != nullptr) { + w->setSources(source->sources()); + w->setImagesPerPage(source->imagesPerPage()); + w->setColumns(source->columns()); + w->setPostFilter(source->postFilter()); } connect(w, &SearchTab::batchAddGroup, m_downloadsTab, &DownloadsTab::batchAddGroup); connect(w, SIGNAL(batchAddUnique(DownloadQueryImage)), m_downloadsTab, SLOT(batchAddUnique(DownloadQueryImage))); @@ -462,8 +470,9 @@ void MainWindow::addSearchTab(SearchTab *w, bool background, bool save) connect(w, &SearchTab::closed, this, &MainWindow::tabClosed); QString title = w->windowTitle(); - if (title.isEmpty()) - { title = "New tab"; } + if (title.isEmpty()) { + title = tr("New tab"); + } int pos = m_loaded ? ui->tabWidget->currentIndex() + (!m_tabs.isEmpty() ? 1 : 0) : m_tabs.count(); int index = ui->tabWidget->insertTab(pos, w, title); @@ -477,11 +486,13 @@ void MainWindow::addSearchTab(SearchTab *w, bool background, bool save) connect(closeTab, &QPushButton::clicked, w, &SearchTab::deleteLater); ui->tabWidget->findChild()->setTabButton(index, QTabBar::RightSide, closeTab); - if (!background) + if (!background) { ui->tabWidget->setCurrentIndex(index); + } - if (save) - saveTabs(m_profile->getPath() + "/tabs.txt"); + if (save) { + saveTabs(m_profile->getPath() + "/tabs.json"); + } } bool MainWindow::saveTabs(const QString &filename) @@ -494,15 +505,16 @@ bool MainWindow::loadTabs(const QString &filename) QList tabs; int currentTab; - if (!TabsLoader::load(filename, tabs, currentTab, m_profile, this)) + if (!TabsLoader::load(filename, tabs, currentTab, m_profile, this)) { return false; + } bool preload = m_settings->value("preloadAllTabs", false).toBool(); - for (auto tab : qAsConst(tabs)) - { + for (auto tab : qAsConst(tabs)) { addSearchTab(tab, true, false); - if (!preload) + if (!preload) { m_tabsWaitingForPreload.append(tab); + } } m_forcedTab = currentTab; @@ -510,25 +522,33 @@ bool MainWindow::loadTabs(const QString &filename) } void MainWindow::updateTabTitle(SearchTab *tab) { - ui->tabWidget->setTabText(ui->tabWidget->indexOf(tab), tab->windowTitle()); + int index = ui->tabWidget->indexOf(tab); + const QString oldText = ui->tabWidget->tabText(index); + const QString newText = tab->windowTitle(); + if (newText != oldText) { + ui->tabWidget->setTabText(index, newText); + } } void MainWindow::updateTabs() { - if (m_loaded) - { - saveTabs(m_profile->getPath() + "/tabs.txt"); + if (m_loaded) { + saveTabs(m_profile->getPath() + "/tabs.json"); } } void MainWindow::tabClosed(SearchTab *tab) { + if (ui == nullptr) { + return; + } + // Store closed tab information QJsonObject obj; tab->write(obj); m_closedTabs.append(obj); - if (m_closedTabs.count() > CLOSED_TAB_HISTORY_MAX) - { + if (m_closedTabs.count() > CLOSED_TAB_HISTORY_MAX) { m_closedTabs.removeFirst(); } + ui->actionRestoreLastClosedTab->setEnabled(true); m_tabs.removeAll(tab); @@ -536,8 +556,9 @@ void MainWindow::tabClosed(SearchTab *tab) } void MainWindow::restoreLastClosedTab() { - if (m_closedTabs.isEmpty()) + if (m_closedTabs.isEmpty()) { return; + } QJsonObject infos = m_closedTabs.takeLast(); SearchTab *tab = TabsLoader::loadTab(infos, m_profile, this, true); @@ -547,19 +568,14 @@ void MainWindow::restoreLastClosedTab() } void MainWindow::currentTabChanged(int tab) { - if (m_loaded && tab < m_tabs.size()) - { + if (m_loaded && tab < m_tabs.size()) { auto currentSearchTab = qobject_cast(ui->tabWidget->currentWidget()); - if (currentSearchTab != nullptr) - { + if (currentSearchTab != nullptr) { SearchTab *tb = m_tabs[tab]; - if (m_tabsWaitingForPreload.contains(tb)) - { + if (m_tabsWaitingForPreload.contains(tb)) { tb->load(); m_tabsWaitingForPreload.removeAll(tb); - } - else if (m_currentTab != currentSearchTab) - { + } else if (m_currentTab != currentSearchTab) { setTags(tb->results()); setWiki(tb->wiki()); } @@ -570,8 +586,9 @@ void MainWindow::currentTabChanged(int tab) void MainWindow::setTags(const QList &tags, SearchTab *from) { - if (from != nullptr && m_tabs.indexOf(from) != ui->tabWidget->currentIndex()) + if (from != nullptr && m_tabs.indexOf(from) != ui->tabWidget->currentIndex()) { return; + } clearLayout(ui->dockInternetScrollLayout); m_currentTags = tags; @@ -594,8 +611,9 @@ void MainWindow::closeCurrentTab() { // Unclosable tabs have a maximum width of 16777214 (default: 16777215) auto currentTab = ui->tabWidget->currentWidget(); - if (currentTab->maximumWidth() != 16777214) - { currentTab->deleteLater(); } + if (currentTab->maximumWidth() != 16777214) { + currentTab->deleteLater(); + } } void MainWindow::tabNext() @@ -614,24 +632,28 @@ void MainWindow::tabPrev() void MainWindow::saveFolder() { QString path = m_settings->value("Save/path").toString().replace("\\", "/"); - if (path.right(1) == "/") - { path = path.left(path.length() - 1); } + if (path.right(1) == "/") { + path = path.left(path.length() - 1); + } QDir dir(path); - if (dir.exists()) - { showInGraphicalShell(path); } + if (dir.exists()) { + showInGraphicalShell(path); + } } void MainWindow::openSettingsFolder() { QDir dir(savePath("")); - if (dir.exists()) - { showInGraphicalShell(dir.absolutePath()); } + if (dir.exists()) { + showInGraphicalShell(dir.absolutePath()); + } } Site *MainWindow::getSelectedSiteOrDefault() { - if (m_selectedSites.isEmpty()) + if (m_selectedSites.isEmpty()) { return m_profile->getSites().first(); + } return m_selectedSites.first(); } @@ -644,21 +666,22 @@ void MainWindow::updateFavorites() QString order = assoc[qMax(ui->comboOrderFav->currentIndex(), 0)]; bool reverse = (ui->comboAscFav->currentIndex() == 1); - if (order == "note") - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByNote); } - else if (order == "lastviewed") - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByLastViewed); } - else - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByName); } - if (reverse) - { m_favorites = reversed(m_favorites); } + if (order == "note") { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByNote); + } else if (order == "lastviewed") { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByLastViewed); + } else { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByName); + } + if (reverse) { + m_favorites = reversed(m_favorites); + } QString format = tr("MM/dd/yyyy"); - for (const Favorite &fav : qAsConst(m_favorites)) - { + for (const Favorite &fav : qAsConst(m_favorites)) { QLabel *lab = new QLabel(QString(R"(%2)").arg(fav.getName(), fav.getName()), this); connect(lab, SIGNAL(linkActivated(QString)), this, SLOT(loadTag(QString))); - lab->setToolTip("
"+tr("Name: %1
Note: %2 %%
Last view: %3").arg(fav.getName(), QString::number(fav.getNote()), fav.getLastViewed().toString(format))); + lab->setToolTip("
" + tr("Name: %1
Note: %2 %%
Last view: %3").arg(fav.getName(), QString::number(fav.getNote()), fav.getLastViewed().toString(format))); ui->layoutFavoritesDock->addWidget(lab); } } @@ -668,8 +691,7 @@ void MainWindow::updateKeepForLater() clearLayout(ui->dockKflScrollLayout); - for (const QString &tag : kfl) - { + for (const QString &tag : kfl) { auto *taglabel = new QAffiche(QString(tag), 0, QColor(), this); taglabel->setText(QString(R"(%1)").arg(tag)); taglabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse); @@ -683,18 +705,14 @@ void MainWindow::updateKeepForLater() void MainWindow::changeEvent(QEvent *event) { // Automatically re-translate UI on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } - // Minimize to tray - else if (event->type() == QEvent::WindowStateChange && (windowState() & Qt::WindowMinimized)) - { + else if (event->type() == QEvent::WindowStateChange && (windowState() & Qt::WindowMinimized)) { bool tray = m_settings->value("Monitoring/enableTray", false).toBool(); bool minimizeToTray = m_settings->value("Monitoring/minimizeToTray", false).toBool(); - if (tray && minimizeToTray && m_trayIcon != nullptr && m_trayIcon->isVisible()) - { + if (tray && minimizeToTray && m_trayIcon != nullptr && m_trayIcon->isVisible()) { QTimer::singleShot(250, this, SLOT(hide())); } } @@ -708,46 +726,44 @@ void MainWindow::closeEvent(QCloseEvent *e) // Close to tray bool tray = m_settings->value("Monitoring/enableTray", false).toBool(); bool closeToTray = m_settings->value("Monitoring/closeToTray", false).toBool(); - if (tray && closeToTray && m_trayIcon != nullptr && m_trayIcon->isVisible() && !m_closeFromTray) - { + if (tray && closeToTray && m_trayIcon != nullptr && m_trayIcon->isVisible() && !m_closeFromTray) { hide(); e->ignore(); return; } // Confirm before closing if there is a batch download or multiple tabs - if (m_settings->value("confirm_close", true).toBool() && (m_tabs.count() > 1 || m_downloadsTab->isDownloading())) - { + if (m_settings->value("confirm_close", true).toBool() && (m_tabs.count() > 1 || m_downloadsTab->isDownloading())) { QMessageBox msgBox(this); msgBox.setText(tr("Are you sure you want to quit?")); msgBox.setIcon(QMessageBox::Warning); QCheckBox dontShowCheckBox(tr("Don't ask me again")); dontShowCheckBox.setCheckable(true); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) - msgBox.setCheckBox(&dontShowCheckBox); -#else - msgBox.addButton(&dontShowCheckBox, QMessageBox::ResetRole); -#endif + #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) + msgBox.setCheckBox(&dontShowCheckBox); + #else + msgBox.addButton(&dontShowCheckBox, QMessageBox::ResetRole); + #endif msgBox.addButton(QMessageBox::Yes); msgBox.addButton(QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); int response = msgBox.exec(); // Don't close on "cancel" - if (response != QMessageBox::Yes) - { + if (response != QMessageBox::Yes) { e->ignore(); return; } // Remember checkbox - if (dontShowCheckBox.checkState() == Qt::Checked) - { m_settings->setValue("confirm_close", false); } + if (dontShowCheckBox.checkState() == Qt::Checked) { + m_settings->setValue("confirm_close", false); + } } log(QStringLiteral("Saving..."), Logger::Debug); m_downloadsTab->saveLinkList(m_profile->getPath() + "/restore.igl"); - saveTabs(m_profile->getPath() + "/tabs.txt"); + saveTabs(m_profile->getPath() + "/tabs.json"); m_settings->setValue("state", saveState()); m_settings->setValue("geometry", saveGeometry()); m_settings->setValue("crashed", false); @@ -758,11 +774,11 @@ void MainWindow::closeEvent(QCloseEvent *e) m_loaded = false; // Ensore the tray icon is hidden quickly on close - if (m_trayIcon != nullptr && m_trayIcon->isVisible()) + if (m_trayIcon != nullptr && m_trayIcon->isVisible()) { m_trayIcon->hide(); + } e->accept(); - qApp->quit(); } void MainWindow::options() @@ -780,8 +796,7 @@ void MainWindow::options() void MainWindow::optionsClosed() { - for (SearchTab *tab : qAsConst(m_tabs)) - { + for (SearchTab *tab : qAsConst(m_tabs)) { tab->optionsChanged(); tab->updateCheckboxes(); } @@ -789,14 +804,16 @@ void MainWindow::optionsClosed() void MainWindow::setSource(const QString &site) { - if (!m_profile->getSites().contains(site)) + if (!m_profile->getSites().contains(site)) { return; + } m_selectedSites.clear(); m_selectedSites.append(m_profile->getSites().value(site)); - if (m_tabs.isEmpty()) + if (m_tabs.isEmpty()) { return; + } m_tabs.first()->saveSources(m_selectedSites); } @@ -848,8 +865,9 @@ void MainWindow::utilTagLoader() void MainWindow::setWiki(const QString &wiki, SearchTab *from) { - if (from != nullptr && from != m_currentTab) + if (from != nullptr && from != m_currentTab) { return; + } ui->labelWiki->setText("" + wiki); } @@ -867,8 +885,7 @@ void MainWindow::tabContextMenuRequested(const QPoint &pos) void MainWindow::on_buttonFolder_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Choose a save folder"), ui->lineFolder->text()); - if (!folder.isEmpty()) - { + if (!folder.isEmpty()) { ui->lineFolder->setText(folder); updateCompleters(); saveSettings(); @@ -877,8 +894,9 @@ void MainWindow::on_buttonFolder_clicked() void MainWindow::on_buttonSaveSettings_clicked() { QString folder = fixFilename("", ui->lineFolder->text()); - if (!QDir(folder).exists()) + if (!QDir(folder).exists()) { QDir::root().mkpath(folder); + } m_settings->setValue("Save/path_real", folder); m_settings->setValue("Save/filename_real", ui->comboFilename->currentText()); @@ -889,14 +907,11 @@ void MainWindow::on_buttonInitSettings_clicked() // Reload filename history QFile f(m_profile->getPath() + "/filenamehistory.txt"); QStringList filenames; - if (f.open(QFile::ReadOnly | QFile::Text)) - { + if (f.open(QFile::ReadOnly | QFile::Text)) { QString line; - while ((line = f.readLine()) > 0) - { + while ((line = f.readLine()) > 0) { QString l = line.trimmed(); - if (!l.isEmpty() && !filenames.contains(l)) - { + if (!l.isEmpty() && !filenames.contains(l)) { filenames.append(l); ui->comboFilename->addItem(l); } @@ -913,13 +928,11 @@ void MainWindow::on_buttonInitSettings_clicked() } void MainWindow::updateCompleters() { - if (ui->lineFolder->text() != m_settings->value("Save/path").toString()) - { + if (ui->lineFolder->text() != m_settings->value("Save/path").toString()) { m_lineFolder_completer.append(ui->lineFolder->text()); ui->lineFolder->setCompleter(new QCompleter(m_lineFolder_completer)); } - /*if (ui->labelFilename->text() != m_settings->value("Save/filename").toString()) - { + /*if (ui->labelFilename->text() != m_settings->value("Save/filename").toString()) { m_lineFilename_completer.append(ui->lineFilename->text()); ui->lineFilename->setCompleter(new QCompleter(m_lineFilename_completer)); }*/ @@ -928,9 +941,11 @@ void MainWindow::saveSettings() { // Filename combobox QString txt = ui->comboFilename->currentText(); - for (int i = ui->comboFilename->count() - 1; i >= 0; --i) - if (ui->comboFilename->itemText(i) == txt) + for (int i = ui->comboFilename->count() - 1; i >= 0; --i) { + if (ui->comboFilename->itemText(i) == txt) { ui->comboFilename->removeItem(i); + } + } ui->comboFilename->insertItem(0, txt); ui->comboFilename->setCurrentIndex(0); QString message; @@ -940,10 +955,10 @@ void MainWindow::saveSettings() // Save filename history QFile f(m_profile->getPath() + "/filenamehistory.txt"); - if (f.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { - for (int i = qMax(0, ui->comboFilename->count() - 50); i < ui->comboFilename->count(); ++i) + if (f.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { + for (int i = qMax(0, ui->comboFilename->count() - 50); i < ui->comboFilename->count(); ++i) { f.write(QString(ui->comboFilename->itemText(i) + "\n").toUtf8()); + } f.close(); } @@ -956,42 +971,42 @@ void MainWindow::saveSettings() -void MainWindow::loadMd5(const QString &path, bool newTab, bool background, bool save) +void MainWindow::loadMd5(const QString &path, bool newTab, bool background, bool save, SearchTab *source) { QFile file(path); - if (file.open(QFile::ReadOnly)) - { + if (file.open(QFile::ReadOnly)) { QString md5 = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5).toHex(); file.close(); - loadTag("md5:" + md5, newTab, background, save); + loadTag("md5:" + md5, newTab, background, save, source); } } -void MainWindow::loadTag(const QString &tag, bool newTab, bool background, bool save) +void MainWindow::loadTag(const QString &tag, bool newTab, bool background, bool save, SearchTab *source) { - if (tag.startsWith("http://") || tag.startsWith("https://")) - { + if (tag.startsWith("http://") || tag.startsWith("https://")) { QDesktopServices::openUrl(tag); return; } - if (newTab) - addTab(tag, background, save); - else if (m_tabs.count() > 0 && ui->tabWidget->currentIndex() < m_tabs.count()) + if (newTab) { + addTab(tag, background, save, source); + } else if (m_tabs.count() > 0 && ui->tabWidget->currentIndex() < m_tabs.count()) { m_tabs[ui->tabWidget->currentIndex()]->setTags(tag); + } } void MainWindow::loadTagTab(const QString &tag) -{ loadTag(tag.isEmpty() ? m_link : tag, true); } +{ loadTag(tag.isEmpty() ? m_link : QUrl::fromPercentEncoding(tag.toUtf8()), true); } void MainWindow::loadTagNoTab(const QString &tag) -{ loadTag(tag.isEmpty() ? m_link : tag, false); } +{ loadTag(tag.isEmpty() ? m_link : QUrl::fromPercentEncoding(tag.toUtf8()), false); } void MainWindow::linkHovered(const QString &tag) { - m_link = tag; + m_link = QUrl::fromPercentEncoding(tag.toUtf8()); } void MainWindow::contextMenu() { - if (m_link.isEmpty()) + if (m_link.isEmpty()) { return; + } TagContextMenu *menu = new TagContextMenu(m_link, m_currentTags, QUrl(), m_profile, false, this); connect(menu, &TagContextMenu::openNewTab, this, &MainWindow::openInNewTab); @@ -1005,8 +1020,7 @@ void MainWindow::openInNewTab() void MainWindow::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) - { + if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) { showNormal(); } } @@ -1029,26 +1043,21 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event) const QMimeData *mimeData = event->mimeData(); // Drop a text containing an URL - if (mimeData->hasText()) - { + if (mimeData->hasText()) { QString url = mimeData->text(); - if (isUrl(url)) - { + if (isUrl(url)) { event->acceptProposedAction(); return; } } // Drop URLs - if (mimeData->hasUrls()) - { + if (mimeData->hasUrls()) { QList urlList = mimeData->urls(); - for (int i = 0; i < urlList.size() && i < 32; ++i) - { + for (int i = 0; i < urlList.size() && i < 32; ++i) { QString path = urlList.at(i).toLocalFile(); QFileInfo fileInfo(path); - if (fileInfo.exists() && fileInfo.isFile()) - { + if (fileInfo.exists() && fileInfo.isFile()) { event->acceptProposedAction(); return; } @@ -1061,18 +1070,15 @@ void MainWindow::dropEvent(QDropEvent *event) const QMimeData *mimeData = event->mimeData(); // Drop a text containing an URL - if (mimeData->hasText()) - { + if (mimeData->hasText()) { QString url = mimeData->text(); - if (isUrl(url)) - { + if (isUrl(url)) { QEventLoop loopLoad; QNetworkReply *reply = m_networkAccessManager.get(QNetworkRequest(QUrl(url))); connect(reply, &QNetworkReply::finished, &loopLoad, &QEventLoop::quit); loopLoad.exec(); - if (reply->error() == QNetworkReply::NoError) - { + if (reply->error() == QNetworkReply::NoError) { QString md5 = QCryptographicHash::hash(reply->readAll(), QCryptographicHash::Md5).toHex(); loadTag("md5:" + md5, true, false); } @@ -1081,11 +1087,9 @@ void MainWindow::dropEvent(QDropEvent *event) } // Drop URLs - if (mimeData->hasUrls()) - { + if (mimeData->hasUrls()) { QList urlList = mimeData->urls(); - for (int i = 0; i < urlList.size() && i < 32; ++i) - { + for (int i = 0; i < urlList.size() && i < 32; ++i) { loadMd5(urlList.at(i).toLocalFile(), true, false); } } diff --git a/gui/src/main-window.h b/gui/src/main-window.h index 205b9334a..239b57682 100644 --- a/gui/src/main-window.h +++ b/gui/src/main-window.h @@ -20,6 +20,7 @@ class SearchTab; class FavoritesTab; class Profile; class DownloadsTab; +class Image; class LogTab; class Favorite; class MonitoringCenter; @@ -58,10 +59,10 @@ class MainWindow : public QMainWindow void updateFavorites(); void updateKeepForLater(); // Tabs - void addTab(const QString &tag = "", bool background = false, bool save = true); - void addPoolTab(int pool = 0, const QString &site = "", bool background = false, bool save = true); - void addGalleryTab(Site *site, QString name, QString id, bool background = false, bool save = true); - void addSearchTab(SearchTab*, bool background = false, bool save = true); + void addTab(const QString &tag = "", bool background = false, bool save = true, SearchTab *source = nullptr); + void addPoolTab(int pool = 0, const QString &site = "", bool background = false, bool save = true, SearchTab *source = nullptr); + void addGalleryTab(Site *site, QSharedPointer gallery, bool background = false, bool save = true, SearchTab *source = nullptr); + void addSearchTab(SearchTab*, bool background = false, bool save = true, SearchTab *source = nullptr); void updateTabTitle(SearchTab*); void tabClosed(SearchTab*); void restoreLastClosedTab(); @@ -74,8 +75,8 @@ class MainWindow : public QMainWindow void tabNext(); void tabPrev(); // Tag list - void loadMd5(const QString &path, bool newTab = true, bool background = true, bool save = true); - void loadTag(const QString &tag, bool newTab = true, bool background = true, bool save = true); + void loadMd5(const QString &path, bool newTab = true, bool background = true, bool save = true, SearchTab *source = nullptr); + void loadTag(const QString &tag, bool newTab = true, bool background = true, bool save = true, SearchTab *source = nullptr); void loadTagTab(const QString &tag); void loadTagNoTab(const QString &tag); void linkHovered(const QString &tag); diff --git a/gui/src/main/main.cpp b/gui/src/main/main.cpp index 1a2c2c6b1..5028d2faa 100644 --- a/gui/src/main/main.cpp +++ b/gui/src/main/main.cpp @@ -26,8 +26,10 @@ #include #include +#include "analytics.h" #include "downloader/downloader.h" #include "functions.h" +#include "logger.h" #include "main-window.h" #include "models/page-api.h" #include "models/profile.h" @@ -42,6 +44,10 @@ #include #include "crashhandler/crashhandler.h" #endif +#if defined(Q_OS_ANDROID) + #include + #include "android.h" +#endif @@ -58,30 +64,40 @@ int main(int argc, char *argv[]) // Set window title according to the current build #ifdef NIGHTLY QString commit(NIGHTLY_COMMIT); - if (!commit.isEmpty()) + if (!commit.isEmpty()) { app.setApplicationDisplayName("Grabber Nightly - " + commit.left(8)); - else + app.setApplicationVersion(QString(VERSION) + " - nightly " + commit.left(8)); + } else { app.setApplicationDisplayName("Grabber Nightly"); + app.setApplicationVersion(QString(VERSION) + " - nightly"); + } #else app.setApplicationDisplayName("Grabber"); #endif // Copy settings files to writable directory QStringList toCopy = QStringList() << "sites/" << "themes/" << "webservices/"; - for (const QString &tgt : toCopy) - { + for (const QString &tgt : toCopy) { const QString from = savePath(tgt, true, false); const QString to = savePath(tgt, true, true); - if (!QDir(to).exists() && QDir(from).exists()) + if (!QDir(to).exists() && QDir(from).exists()) { copyRecursively(from, to); + } } + #if defined(Q_OS_ANDROID) + if (!checkPermission("android.permission.WRITE_EXTERNAL_STORAGE")) { + QMessageBox::critical(nullptr, "Permission error", "Grabber needs storage writing permissions to download images"); + return 0; + } + #endif + QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); #if !defined(USE_CLI) - const QCommandLineOption cliOption(QStringList() << "c" << "cli", "Disable the GUI."); + const QCommandLineOption cliOption(QStringList() << "c" << "cli", "Disable the GUI."); parser.addOption(cliOption); #endif const QCommandLineOption tagsOption(QStringList() << "t" << "tags", "Tags to search for.", "tags"); @@ -94,6 +110,7 @@ int main(int argc, char *argv[]) const QCommandLineOption userOption(QStringList() << "u" << "user", "Username to connect to the source.", "user"); const QCommandLineOption passwordOption(QStringList() << "w" << "password", "Password to connect to the source.", "password"); const QCommandLineOption blacklistOption(QStringList() << "b" << "blacklist", "Download blacklisted images."); + const QCommandLineOption tagsBlacklistOption(QStringList() << "tb" << "tags-blacklist" , "Tags to remove from results.", "tags-blacklist"); const QCommandLineOption postfilteringOption(QStringList() << "r" << "postfilter", "Filter results.", "filter"); const QCommandLineOption noDuplicatesOption(QStringList() << "n" << "no-duplicates", "Remove duplicates from results."); const QCommandLineOption verboseOption(QStringList() << "d" << "debug", "Show debug messages."); @@ -109,6 +126,7 @@ int main(int argc, char *argv[]) parser.addOption(userOption); parser.addOption(passwordOption); parser.addOption(blacklistOption); + parser.addOption(tagsBlacklistOption); parser.addOption(postfilteringOption); parser.addOption(tagsMinOption); parser.addOption(tagsFormatOption); @@ -133,29 +151,36 @@ int main(int argc, char *argv[]) const bool gui = false; #endif - const bool verbose = parser.isSet(verboseOption); + const bool verbose = parser.isSet(verboseOption); #if !defined(QT_DEBUG) Logger::setupMessageOutput(gui || verbose); #endif - if (verbose) + if (verbose) { Logger::getInstance().setLogLevel(Logger::Debug); + } #if defined(USE_BREAKPAD) && !defined(USE_CLI) - if (gui) - { + if (gui) { QDir dir = QFileInfo(argv[0]).dir(); QString crashes = savePath("crashes"); - if (!dir.exists(crashes)) - { dir.mkpath(crashes); } + if (!dir.exists(crashes)) { + dir.mkpath(crashes); + } CrashHandler::instance()->Init(crashes); } #endif Profile *profile = new Profile(savePath()); profile->purgeTemp(24 * 60 * 60); + QSettings *settings = profile->getSettings(); - if (!gui) - { + // Analytics + Analytics::getInstance().setTrackingID("UA-22768717-6"); + Analytics::getInstance().setEnabled(settings->value("send_usage_data", true).toBool()); + Analytics::getInstance().sendEvent("lifecycle", "start"); + + if (!gui) { + QString blacklistOverride = parser.value(tagsBlacklistOption); Downloader *dwnldr = new Downloader(profile, parser.value(tagsOption).split(" ", QString::SkipEmptyParts), parser.value(postfilteringOption).split(" ", QString::SkipEmptyParts), @@ -168,36 +193,34 @@ int main(int argc, char *argv[]) parser.value(userOption), parser.value(passwordOption), parser.isSet(blacklistOption), - profile->getBlacklist(), + blacklistOverride.isEmpty() ? profile->getBlacklist() : Blacklist(blacklistOverride.split(' ')), parser.isSet(noDuplicatesOption), parser.value(tagsMinOption).toInt(), parser.value(tagsFormatOption)); - if (parser.isSet(returnCountOption)) + if (parser.isSet(returnCountOption)) { dwnldr->getPageCount(); - else if (parser.isSet(returnTagsOption)) + } else if (parser.isSet(returnTagsOption)) { dwnldr->getPageTags(); - else if (parser.isSet(returnPureTagsOption)) + } else if (parser.isSet(returnPureTagsOption)) { dwnldr->getTags(); - else if (parser.isSet(returnImagesOption)) + } else if (parser.isSet(returnImagesOption)) { dwnldr->getUrls(); - else if (parser.isSet(downloadOption)) + } else if (parser.isSet(downloadOption)) { dwnldr->getImages(); - else + } else { parser.showHelp(); + } dwnldr->setQuit(true); QObject::connect(dwnldr, &Downloader::quit, qApp, &QApplication::quit); } #if !defined(USE_CLI) - else - { + else { // Check for updates - QSettings *settings = profile->getSettings(); - const int cfuInterval = settings->value("check_for_updates", 24*60*60).toInt(); + const int cfuInterval = settings->value("check_for_updates", 24 * 60 * 60).toInt(); QDateTime lastCfu = settings->value("last_check_for_updates", QDateTime()).toDateTime(); - if (cfuInterval >= 0 && (!lastCfu.isValid() || lastCfu.addSecs(cfuInterval) <= QDateTime::currentDateTime())) - { + if (cfuInterval >= 0 && (!lastCfu.isValid() || lastCfu.addSecs(cfuInterval) <= QDateTime::currentDateTime())) { settings->setValue("last_check_for_updates", QDateTime::currentDateTime()); bool shouldQuit = false; @@ -211,8 +234,9 @@ int main(int argc, char *argv[]) el->deleteLater(); updateDialog->deleteLater(); - if (shouldQuit) + if (shouldQuit) { return 0; + } } QMap params; diff --git a/gui/src/monitoring-center.cpp b/gui/src/monitoring-center.cpp index 31fe2552b..55223b9a0 100644 --- a/gui/src/monitoring-center.cpp +++ b/gui/src/monitoring-center.cpp @@ -16,7 +16,7 @@ MonitoringCenter::MonitoringCenter(Profile *profile, QSystemTrayIcon *trayIcon, QObject *parent) : QObject(parent), m_profile(profile), m_trayIcon(trayIcon) -{ } +{} void MonitoringCenter::start() { @@ -41,22 +41,22 @@ void MonitoringCenter::checkMonitor(Monitor &monitor, const Favorite &favorite) // Count new images int newImages = 0; int count = page->images().count(); - for (const QSharedPointer &img : page->images()) - { - if (img->createdAt() > monitor.lastCheck()) - { newImages++; } + for (const QSharedPointer &img : page->images()) { + if (img->createdAt() > monitor.lastCheck()) { + newImages++; + } } // Send notification - if (newImages > 0 && m_trayIcon != nullptr && m_trayIcon->isVisible()) - { + if (newImages > 0 && m_trayIcon != nullptr && m_trayIcon->isVisible()) { QString msg; - if (count == 1) - { msg = tr("New images found for tag '%1' on '%2'"); } - else if (newImages < count) - { msg = tr("%n new image(s) found for tag '%1' on '%2'", "", newImages); } - else - { msg = tr("More than %n new image(s) found for tag '%1' on '%2'", "", newImages); } + if (count == 1) { + msg = tr("New images found for tag '%1' on '%2'"); + } else if (newImages < count) { + msg = tr("%n new image(s) found for tag '%1' on '%2'", "", newImages); + } else { + msg = tr("More than %n new image(s) found for tag '%1' on '%2'", "", newImages); + } m_trayIcon->showMessage(tr("Grabber monitoring"), msg.arg(favorite.getName(), site->name()), QSystemTrayIcon::Information); } @@ -64,44 +64,43 @@ void MonitoringCenter::checkMonitor(Monitor &monitor, const Favorite &favorite) monitor.setLastCheck(QDateTime::currentDateTimeUtc()); monitor.setCumulated(monitor.cumulated() + newImages, count != 1 && newImages < count); - if (newImages > 0) - { emit m_profile->favoritesChanged(); } + if (newImages > 0) { + emit m_profile->favoritesChanged(); + } } void MonitoringCenter::tick() { - if (m_stop) + if (m_stop) { return; + } int minNextMonitoring = -1; log(QStringLiteral("Monitoring tick"), Logger::Info); - for (Favorite &fav : m_profile->getFavorites()) - { - for (Monitor &monitor : fav.getMonitors()) - { + for (Favorite &fav : m_profile->getFavorites()) { + for (Monitor &monitor : fav.getMonitors()) { // If this favorite's monitoring expired, we check it for updates int next = monitor.secsToNextCheck(); - if (next <= 0) - { + if (next <= 0) { checkMonitor(monitor, fav); next = monitor.secsToNextCheck(); } // Only keep the soonest expiring timeout - if (next < minNextMonitoring || minNextMonitoring == -1) - { minNextMonitoring = next; } + if (next < minNextMonitoring || minNextMonitoring == -1) { + minNextMonitoring = next; + } } } // Re-run this method as soon as one of the monitoring timeout expires - if (minNextMonitoring > 0) - { + if (minNextMonitoring > 0) { log(QStringLiteral("Next monitoring will be in %1 seconds").arg(minNextMonitoring), Logger::Info); QTimer::singleShot(minNextMonitoring * 1000, this, SLOT(tick())); + } else { + log(QStringLiteral("Monitoring finished"), Logger::Info); } - else - { log(QStringLiteral("Monitoring finished"), Logger::Info); } } void MonitoringCenter::stop() diff --git a/gui/src/pch.cpp b/gui/src/pch.cpp deleted file mode 100644 index 2cd79060c..000000000 --- a/gui/src/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "src/pch.h" diff --git a/gui/src/pch.h b/gui/src/pch.h deleted file mode 100644 index 63e32a5af..000000000 --- a/gui/src/pch.h +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#ifdef Q_OS_WIN - #include -#endif diff --git a/gui/src/search-window.cpp b/gui/src/search-window.cpp index 57e2d31d7..c1451ade8 100644 --- a/gui/src/search-window.cpp +++ b/gui/src/search-window.cpp @@ -28,20 +28,10 @@ SearchWindow::SearchWindow(QString tags, Profile *profile, QWidget *parent) connect(m_calendar, &QCalendarWidget::activated, m_calendar, &QCalendarWidget::close); connect(ui->buttonCalendar, &QPushButton::clicked, m_calendar, &QCalendarWidget::show); - QStringList favorites; - favorites.reserve(profile->getFavorites().count()); - for (const Favorite &fav : profile->getFavorites()) - favorites.append(fav.getName()); m_tags = new TextEdit(profile, this); m_tags->setContextMenuPolicy(Qt::CustomContextMenu); - QStringList completion; - completion.append(profile->getAutoComplete()); - completion.append(profile->getCustomAutoComplete()); - completion.append(favorites); - completion.removeDuplicates(); - completion.sort(); - auto *completer = new QCompleter(completion, m_tags); - completer->setCaseSensitivity(Qt::CaseInsensitive); + auto *completer = new QCompleter(profile->getAutoComplete(), m_tags); + completer->setCaseSensitivity(Qt::CaseInsensitive); m_tags->setCompleter(completer); connect(m_tags, &TextEdit::returnPressed, this, &SearchWindow::accept); ui->formLayout->setWidget(0, QFormLayout::FieldRole, m_tags); @@ -50,29 +40,25 @@ SearchWindow::SearchWindow(QString tags, Profile *profile, QWidget *parent) QStringList ratings = QStringList() << "rating:safe" << "-rating:safe" << "rating:questionable" << "-rating:questionable" << "rating:explicit" << "-rating:explicit"; QStringList status = QStringList() << "deleted" << "active" << "flagged" << "pending" << "any"; - if (tags.contains("order:")) - { + if (tags.contains("order:")) { QRegularExpression reg("order:([^ ]+)"); auto match = reg.match(tags); ui->comboOrder->setCurrentIndex(orders.indexOf(match.captured(1)) + 1); tags.remove(match.captured(0)); } - if (tags.contains("rating:")) - { + if (tags.contains("rating:")) { QRegularExpression reg("-?rating:[^ ]+"); auto match = reg.match(tags); ui->comboRating->setCurrentIndex(ratings.indexOf(match.captured(0)) + 1); tags.remove(match.captured(0)); } - if (tags.contains("status:")) - { + if (tags.contains("status:")) { QRegularExpression reg("status:([^ ]+)"); auto match = reg.match(tags); ui->comboStatus->setCurrentIndex(status.indexOf(match.captured(1)) + 1); tags.remove(match.captured(0)); } - if (tags.contains("date:")) - { + if (tags.contains("date:")) { QRegularExpression reg("date:([^ ]+)"); auto match = reg.match(tags); m_calendar->setSelectedDate(QDate::fromString(match.captured(1), QStringLiteral("MM/dd/yyyy"))); @@ -95,14 +81,18 @@ QString SearchWindow::generateSearch(const QString &additional) const QString prefix = !additional.isEmpty() ? additional + " " : QString(); QString search = prefix + m_tags->toPlainText(); - if (ui->comboStatus->currentIndex() != 0) + if (ui->comboStatus->currentIndex() != 0) { search += " status:" + status.at(ui->comboStatus->currentIndex() - 1); - if (ui->comboOrder->currentIndex() != 0) + } + if (ui->comboOrder->currentIndex() != 0) { search += " order:" + orders.at(ui->comboOrder->currentIndex() - 1); - if (ui->comboRating->currentIndex() != 0) + } + if (ui->comboRating->currentIndex() != 0) { search += " " + ratings.at(ui->comboRating->currentIndex() - 1); - if (!ui->lineDate->text().isEmpty()) + } + if (!ui->lineDate->text().isEmpty()) { search += " date:" + ui->lineDate->text(); + } return search.trimmed(); } @@ -123,8 +113,7 @@ void SearchWindow::on_buttonImage_clicked() QString path = QFileDialog::getOpenFileName(this, tr("Search an image"), m_profile->getSettings()->value("Save/path").toString(), QStringLiteral("Images (*.png *.gif *.jpg *.jpeg)")); QFile f(path); QString md5; - if (f.exists()) - { + if (f.exists()) { f.open(QFile::ReadOnly); md5 = QCryptographicHash::hash(f.readAll(), QCryptographicHash::Md5).toHex(); } diff --git a/gui/src/search-window.ui b/gui/src/search-window.ui index ec390fd4a..e2781e33c 100644 --- a/gui/src/search-window.ui +++ b/gui/src/search-window.ui @@ -20,7 +20,7 @@ Search
- + :/images/icon.ico:/images/icon.ico @@ -276,9 +276,7 @@ buttonBox - - - + diff --git a/gui/src/settings/filename-window.cpp b/gui/src/settings/filename-window.cpp index 732fb05b0..68e131814 100644 --- a/gui/src/settings/filename-window.cpp +++ b/gui/src/settings/filename-window.cpp @@ -26,15 +26,12 @@ FilenameWindow::FilenameWindow(Profile *profile, QString value, QWidget *parent) connect(ui->radioJavascript, &QRadioButton::toggled, m_scintilla, &QWidget::setEnabled); ui->verticalLayout->insertWidget(ui->verticalLayout->count() - 1, m_scintilla); - if (value.startsWith("javascript:")) - { + if (value.startsWith("javascript:")) { value = value.right(value.length() - 11); m_scintilla->setText(value); ui->lineClassic->setEnabled(false); ui->radioJavascript->toggle(); - } - else - { + } else { ui->lineClassic->setText(value); m_scintilla->setEnabled(false); ui->radioClassic->toggle(); @@ -59,26 +56,25 @@ void FilenameWindow::on_lineClassic_textChanged(QString text) QRegExp date("%date:format=([^%]+)%"); int pos = 0; - while ((pos = date.indexIn(text, pos)) != -1) - { + while ((pos = date.indexIn(text, pos)) != -1) { QString cap = date.cap(1); QString format; - for (const QChar &c : cap) - { - if (c == 'Y') - { format += "' + date.getFullYear() + '"; } - else if (c == 'M') - { format += "' + date.getMonth() + '"; } - else if (c == 'd') - { format += "' + date.getDate() + '"; } - else if (c == 'h') - { format += "' + date.getHours() + '"; } - else if (c == 'm') - { format += "' + date.getMinutes() + '"; } - else if (c == 's') - { format += "' + date.getSeconds() + '"; } - else - { format += c; } + for (const QChar &c : cap) { + if (c == 'Y') { + format += "' + date.getFullYear() + '"; + } else if (c == 'M') { + format += "' + date.getMonth() + '"; + } else if (c == 'd') { + format += "' + date.getDate() + '"; + } else if (c == 'h') { + format += "' + date.getHours() + '"; + } else if (c == 'm') { + format += "' + date.getMinutes() + '"; + } else if (c == 's') { + format += "' + date.getSeconds() + '"; + } else { + format += c; + } } text = text.left(pos) + format + text.mid(pos + date.matchedLength()); @@ -86,14 +82,18 @@ void FilenameWindow::on_lineClassic_textChanged(QString text) } QString value = "'" + text.replace(QRegularExpression("%([^%]+)%"), "' + \\1 + '").remove(" + '' + ").trimmed() + "'"; - if (value.startsWith("' + ")) - { value = value.right(value.length() - 4); } - if (value.startsWith("'' + ")) - { value = value.right(value.length() - 5); } - if (value.endsWith(" + '")) - { value = value.left(value.length() - 4); } - if (value.endsWith(" + ''")) - { value = value.left(value.length() - 5); } + if (value.startsWith("' + ")) { + value = value.right(value.length() - 4); + } + if (value.startsWith("'' + ")) { + value = value.right(value.length() - 5); + } + if (value.endsWith(" + '")) { + value = value.left(value.length() - 4); + } + if (value.endsWith(" + ''")) { + value = value.left(value.length() - 5); + } m_scintilla->setText(value); } @@ -109,8 +109,7 @@ void FilenameWindow::on_buttonHelpJavascript_clicked() QString FilenameWindow::format() const { - if (ui->radioJavascript->isChecked()) - { + if (ui->radioJavascript->isChecked()) { #if defined(USE_QSCINTILLA) return "javascript:" + m_scintilla->text(); #else @@ -125,8 +124,7 @@ void FilenameWindow::done(int r) { QMap sites = m_profile->getSites(); - if (QDialog::Accepted == r && ui->radioJavascript->isChecked() && !sites.isEmpty()) - { + if (QDialog::Accepted == r && ui->radioJavascript->isChecked() && !sites.isEmpty()) { Site *site = sites.first(); QMap info; @@ -137,13 +135,11 @@ void FilenameWindow::done(int r) info.insert("tags_copyright", "copyright_1 copyright_2"); Image image(site, info, m_profile); - QStringList det = image.path(format(), QString()); + QStringList det = image.paths(format(), QString(), 0); - if (det.isEmpty()) - { + if (det.isEmpty()) { const int reply = QMessageBox::question(this, tr("Warning"), tr("You script contains error, are you sure you want to save it?"), QMessageBox::Yes | QMessageBox::Cancel); - if (reply == QMessageBox::Cancel) - { + if (reply == QMessageBox::Cancel) { return; } } diff --git a/gui/src/settings/log-window.cpp b/gui/src/settings/log-window.cpp index 8565b78cc..98b7b9a36 100644 --- a/gui/src/settings/log-window.cpp +++ b/gui/src/settings/log-window.cpp @@ -9,8 +9,7 @@ LogWindow::LogWindow(int index, Profile *profile, QWidget *parent) { ui->setupUi(this); - if (index >= 0) - { + if (index >= 0) { auto logFiles = getExternalLogFiles(m_profile->getSettings()); auto dta = logFiles[index]; diff --git a/gui/src/settings/options-window.cpp b/gui/src/settings/options-window.cpp index 568b0b4fe..dc2642037 100644 --- a/gui/src/settings/options-window.cpp +++ b/gui/src/settings/options-window.cpp @@ -10,6 +10,7 @@ #include "functions.h" #include "helpers.h" #include "language-loader.h" +#include "logger.h" #include "models/profile.h" #include "models/site.h" #include "reverse-search/reverse-search-loader.h" @@ -17,6 +18,7 @@ #include "settings/custom-window.h" #include "settings/filename-window.h" #include "settings/log-window.h" +#include "settings/token-settings-widget.h" #include "settings/web-service-window.h" #include "theme-loader.h" @@ -27,19 +29,22 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); + QSettings *settings = profile->getSettings(); + ui->splitter->setSizes(QList() << 160 << ui->stackedWidget->sizeHint().width()); ui->splitter->setStretchFactor(0, 0); ui->splitter->setStretchFactor(1, 1); LanguageLoader languageLoader(savePath("languages/", true)); QMap languages = languageLoader.getAllLanguages(); - for (auto it = languages.constBegin(); it != languages.constEnd(); ++it) - { ui->comboLanguages->addItem(it.value(), it.key()); } + for (auto it = languages.constBegin(); it != languages.constEnd(); ++it) { + ui->comboLanguages->addItem(it.value(), it.key()); + } - QSettings *settings = profile->getSettings(); ui->comboLanguages->setCurrentText(languages[settings->value("language", "English").toString()]); ui->lineWhitelist->setText(settings->value("whitelistedtags").toString()); ui->lineAdd->setText(settings->value("add").toString()); + ui->lineGlobalPostFilter->setText(settings->value("globalPostFilter").toString()); QStringList wl = QStringList() << "never" << "image" << "page"; ui->comboWhitelist->setCurrentIndex(wl.indexOf(settings->value("whitelist_download", "image").toString())); ui->lineIgnored->setText(settings->value("ignoredtags").toString()); @@ -53,6 +58,7 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) ui->checkGetUnloadedPages->setChecked(settings->value("getunloadedpages", false).toBool()); ui->checkInvertToggle->setChecked(settings->value("invertToggle", false).toBool()); ui->checkConfirmClose->setChecked(settings->value("confirm_close", true).toBool()); + ui->checkSendUsageData->setChecked(settings->value("send_usage_data", true).toBool()); QList checkForUpdates = QList() << 0 << 24 * 60 * 60 << 7 * 24 * 60 * 60 << 30 * 24 * 60 * 60 << -1; ui->comboCheckForUpdates->setCurrentIndex(checkForUpdates.indexOf(settings->value("check_for_updates", 24 * 60 * 60).toInt())); @@ -65,14 +71,13 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) ui->comboSource4->setCurrentIndex(sources.indexOf(settings->value("source_4", "rss").toString())); ui->spinAutoTagAdd->setValue(settings->value("tagsautoadd", 10).toInt()); - QMap> filenames = getFilenames(settings); + QList filenames = getFilenames(settings); m_filenamesConditions = QList(); m_filenamesFilenames = QList(); - for (auto it = filenames.constBegin(); it != filenames.constEnd(); ++it) - { - auto leCondition = new QLineEdit(it.key()); - auto leFilename = new QLineEdit(it.value().first); - auto leFolder = new QLineEdit(it.value().second); + for (const auto &fn : filenames) { + auto leCondition = new QLineEdit(fn.condition); + auto leFilename = new QLineEdit(fn.filename.format()); + auto leFolder = new QLineEdit(fn.path); m_filenamesConditions.append(leCondition); m_filenamesFilenames.append(leFilename); @@ -148,76 +153,26 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) ui->lineSeparator->setText(settings->value("separator", " ").toString()); ui->checkNoJpeg->setChecked(settings->value("noJpeg", true).toBool()); - ui->lineArtistsIfNone->setText(settings->value("artist_empty", "anonymous").toString()); - ui->spinArtistsMoreThanN->setValue(settings->value("artist_multiple_limit", 1).toInt()); - ui->spinArtistsKeepN->setValue(settings->value("artist_multiple_keepN", 1).toInt()); - ui->spinArtistsKeepNThenAdd->setValue(settings->value("artist_multiple_keepNThenAdd_keep", 1).toInt()); - ui->lineArtistsKeepNThenAdd->setText(settings->value("artist_multiple_keepNThenAdd_add", " (+ %count%)").toString()); - ui->lineArtistsSeparator->setText(settings->value("artist_sep", "+").toString()); - ui->lineArtistsReplaceAll->setText(settings->value("artist_value", "multiple artists").toString()); - const QString artistMultiple = settings->value("artist_multiple", "replaceAll").toString(); - if (artistMultiple == "keepAll") { ui->radioArtistsKeepAll->setChecked(true); } - else if (artistMultiple == "keepN") { ui->radioArtistsKeepN->setChecked(true); } - else if (artistMultiple == "keepNThenAdd") { ui->radioArtistsKeepNThenAdd->setChecked(true); } - else if (artistMultiple == "replaceAll") { ui->radioArtistsReplaceAll->setChecked(true); } - else if (artistMultiple == "multiple") { ui->radioArtistsMultiple->setChecked(true); } - - ui->lineCopyrightsIfNone->setText(settings->value("copyright_empty", "misc").toString()); - ui->checkCopyrightsUseShorter->setChecked(settings->value("copyright_useshorter", true).toBool()); - ui->spinCopyrightsMoreThanN->setValue(settings->value("copyright_multiple_limit", 1).toInt()); - ui->spinCopyrightsKeepN->setValue(settings->value("copyright_multiple_keepN", 1).toInt()); - ui->spinCopyrightsKeepNThenAdd->setValue(settings->value("copyright_multiple_keepNThenAdd_keep", 1).toInt()); - ui->lineCopyrightsKeepNThenAdd->setText(settings->value("copyright_multiple_keepNThenAdd_add", " (+ %count%)").toString()); - ui->lineCopyrightsSeparator->setText(settings->value("copyright_sep", "+").toString()); - ui->lineCopyrightsReplaceAll->setText(settings->value("copyright_value", "crossover").toString()); - const QString copyrightMultiple = settings->value("copyright_multiple", "replaceAll").toString(); - if (copyrightMultiple == "keepAll") { ui->radioCopyrightsKeepAll->setChecked(true); } - else if (copyrightMultiple == "keepN") { ui->radioCopyrightsKeepN->setChecked(true); } - else if (copyrightMultiple == "keepNThenAdd") { ui->radioCopyrightsKeepNThenAdd->setChecked(true); } - else if (copyrightMultiple == "replaceAll") { ui->radioCopyrightsReplaceAll->setChecked(true); } - else if (copyrightMultiple == "multiple") { ui->radioCopyrightsMultiple->setChecked(true); } - - ui->lineCharactersIfNone->setText(settings->value("character_empty", "unknown").toString()); - ui->spinCharactersMoreThanN->setValue(settings->value("character_multiple_limit", 1).toInt()); - ui->spinCharactersKeepN->setValue(settings->value("character_multiple_keepN", 1).toInt()); - ui->spinCharactersKeepNThenAdd->setValue(settings->value("character_multiple_keepNThenAdd_keep", 1).toInt()); - ui->lineCharactersKeepNThenAdd->setText(settings->value("character_multiple_keepNThenAdd_add", " (+ %count%)").toString()); - ui->lineCharactersSeparator->setText(settings->value("character_sep", "+").toString()); - ui->lineCharactersReplaceAll->setText(settings->value("character_value", "group").toString()); - const QString characterMultiple = settings->value("character_multiple", "replaceAll").toString(); - if (characterMultiple == "keepAll") { ui->radioCharactersKeepAll->setChecked(true); } - else if (characterMultiple == "keepN") { ui->radioCharactersKeepN->setChecked(true); } - else if (characterMultiple == "keepNThenAdd") { ui->radioCharactersKeepNThenAdd->setChecked(true); } - else if (characterMultiple == "replaceAll") { ui->radioCharactersReplaceAll->setChecked(true); } - else if (characterMultiple == "multiple") { ui->radioCharactersMultiple->setChecked(true); } - - ui->lineSpeciesIfNone->setText(settings->value("species_empty", "unknown").toString()); - ui->spinSpeciesMoreThanN->setValue(settings->value("species_multiple_limit", 1).toInt()); - ui->spinSpeciesKeepN->setValue(settings->value("species_multiple_keepN", 1).toInt()); - ui->spinSpeciesKeepNThenAdd->setValue(settings->value("species_multiple_keepNThenAdd_keep", 1).toInt()); - ui->lineSpeciesKeepNThenAdd->setText(settings->value("species_multiple_keepNThenAdd_add", " (+ %count%)").toString()); - ui->lineSpeciesSeparator->setText(settings->value("species_sep", "+").toString()); - ui->lineSpeciesReplaceAll->setText(settings->value("species_value", "multiple").toString()); - const QString speciesMultiple = settings->value("species_multiple", "keepAll").toString(); - if (speciesMultiple == "keepAll") { ui->radioSpeciesKeepAll->setChecked(true); } - else if (speciesMultiple == "keepN") { ui->radioSpeciesKeepN->setChecked(true); } - else if (speciesMultiple == "keepNThenAdd") { ui->radioSpeciesKeepNThenAdd->setChecked(true); } - else if (speciesMultiple == "replaceAll") { ui->radioSpeciesReplaceAll->setChecked(true); } - else if (speciesMultiple == "multiple") { ui->radioSpeciesMultiple->setChecked(true); } - - ui->lineMetasIfNone->setText(settings->value("meta_empty", "none").toString()); - ui->spinMetasMoreThanN->setValue(settings->value("meta_multiple_limit", 1).toInt()); - ui->spinMetasKeepN->setValue(settings->value("meta_multiple_keepN", 1).toInt()); - ui->spinMetasKeepNThenAdd->setValue(settings->value("meta_multiple_keepNThenAdd_keep", 1).toInt()); - ui->lineMetasKeepNThenAdd->setText(settings->value("meta_multiple_keepNThenAdd_add", " (+ %count%)").toString()); - ui->lineMetasSeparator->setText(settings->value("meta_sep", "+").toString()); - ui->lineMetasReplaceAll->setText(settings->value("meta_value", "multiple").toString()); - const QString metaMultiple = settings->value("meta_multiple", "keepAll").toString(); - if (metaMultiple == "keepAll") { ui->radioMetasKeepAll->setChecked(true); } - else if (metaMultiple == "keepN") { ui->radioMetasKeepN->setChecked(true); } - else if (metaMultiple == "keepNThenAdd") { ui->radioMetasKeepNThenAdd->setChecked(true); } - else if (metaMultiple == "replaceAll") { ui->radioMetasReplaceAll->setChecked(true); } - else if (metaMultiple == "multiple") { ui->radioMetasMultiple->setChecked(true); } + + // Build the "tags" settings + auto tagsTree = ui->treeWidget->invisibleRootItem()->child(2)->child(4); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Artist", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Copyright", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Character", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Model", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Photo set", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Species", tagsTree->type())); + tagsTree->addChild(new QTreeWidgetItem(QStringList() << "Meta", tagsTree->type())); + m_tokenSettings.append(new TokenSettingsWidget(settings, "artist", false, "anonymous", "multiple artists", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "copyright", true, "misc", "crossover", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "character", false, "unknown", "group", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "model", false, "unknown", "multiple", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "photo_set", false, "unknown", "multiple", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "species", false, "unknown", "multiple", this)); + m_tokenSettings.append(new TokenSettingsWidget(settings, "meta", false, "none", "multiple", this)); + for (int i = 0; i < m_tokenSettings.count(); ++i) { + ui->stackedWidget->insertWidget(i + 8, m_tokenSettings[i]); + } ui->spinLimit->setValue(settings->value("limit", 0).toInt()); ui->spinSimultaneous->setValue(settings->value("simultaneous", 1).toInt()); @@ -228,8 +183,7 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) m_customNames = QList(); m_customTags = QList(); i = 0; - for (auto it = customs.constBegin(); it != customs.constEnd(); ++it) - { + for (auto it = customs.constBegin(); it != customs.constEnd(); ++it) { auto *leName = new QLineEdit(it.key()); auto *leTags = new QLineEdit(it.value().join(" ")); m_customNames.append(leName); @@ -240,8 +194,9 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) // Themes ThemeLoader themeLoader(savePath("themes/", true)); QStringList themes = themeLoader.getAllThemes(); - for (const QString &theme : themes) - { ui->comboTheme->addItem(theme, theme); } + for (const QString &theme : themes) { + ui->comboTheme->addItem(theme, theme); + } ui->comboTheme->setCurrentText(settings->value("theme", "Default").toString()); ui->checkSingleDetailsWindow->setChecked(settings->value("Zoom/singleWindow", false).toBool()); @@ -254,7 +209,7 @@ OptionsWindow::OptionsWindow(Profile *profile, QWidget *parent) ui->checkImageCloseMiddleClick->setChecked(settings->value("imageCloseMiddleClick", true).toBool()); ui->checkImageNavigateScroll->setChecked(settings->value("imageNavigateScroll", true).toBool()); ui->checkZoomShowTagCount->setChecked(settings->value("Zoom/showTagCount", false).toBool()); - //ui->checkZoomViewSamples->setChecked(settings->value("Zoom/viewSamples", false).toBool()); + ui->checkZoomViewSamples->setChecked(settings->value("Zoom/viewSamples", false).toBool()); QStringList imageTagOrder = QStringList() << "type" << "name" << "count"; ui->comboImageTagOrder->setCurrentIndex(imageTagOrder.indexOf(settings->value("Zoom/tagOrder", "type").toString())); QStringList positionsV = QStringList() << "top" << "center" << "bottom"; @@ -352,14 +307,16 @@ void OptionsWindow::on_comboSourcesLetters_currentIndexChanged(int i) void OptionsWindow::on_buttonFolder_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Choose a save folder"), ui->lineFolder->text()); - if (!folder.isEmpty()) - { ui->lineFolder->setText(folder); } + if (!folder.isEmpty()) { + ui->lineFolder->setText(folder); + } } void OptionsWindow::on_buttonFolderFavorites_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Choose a save folder for favorites"), ui->lineFolderFavorites->text()); - if (!folder.isEmpty()) - { ui->lineFolderFavorites->setText(folder); } + if (!folder.isEmpty()) { + ui->lineFolderFavorites->setText(folder); + } } void OptionsWindow::on_buttonFilenamePlus_clicked() @@ -422,8 +379,7 @@ void OptionsWindow::showLogFiles(QSettings *settings) auto *mapperRemoveLogFile = new QSignalMapper(this); connect(mapperEditLogFile, SIGNAL(mapped(int)), this, SLOT(editLogFile(int))); connect(mapperRemoveLogFile, SIGNAL(mapped(int)), this, SLOT(removeLogFile(int))); - for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) - { + for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) { const int i = it.key(); auto logFile = it.value(); @@ -462,8 +418,9 @@ void OptionsWindow::removeLogFile(int index) QSettings *settings = m_profile->getSettings(); settings->beginGroup("LogFiles"); settings->beginGroup(QString::number(index)); - for (const QString &key : settings->childKeys()) - { settings->remove(key); } + for (const QString &key : settings->childKeys()) { + settings->remove(key); + } settings->endGroup(); settings->endGroup(); @@ -475,19 +432,20 @@ void OptionsWindow::setLogFile(int index, const QMap &logFile QSettings *settings = m_profile->getSettings(); settings->beginGroup("LogFiles"); - if (index < 0) - { + if (index < 0) { auto childGroups = settings->childGroups(); - if (childGroups.isEmpty()) - { index = 0; } - else - { index = childGroups.last().toInt() + 1; } + if (childGroups.isEmpty()) { + index = 0; + } else { + index = childGroups.last().toInt() + 1; + } } settings->beginGroup(QString::number(index)); - for (auto it = logFile.constBegin(); it != logFile.constEnd(); ++it) - { settings->setValue(it.key(), it.value()); } + for (auto it = logFile.constBegin(); it != logFile.constEnd(); ++it) { + settings->setValue(it.key(), it.value()); + } settings->endGroup(); settings->endGroup(); @@ -510,8 +468,7 @@ void OptionsWindow::showWebServices() connect(mapperMoveDownWebService, SIGNAL(mapped(int)), this, SLOT(moveDownWebService(int))); m_webServicesIds.clear(); - for (int j = 0; j < m_webServices.count(); ++j) - { + for (int j = 0; j < m_webServices.count(); ++j) { auto webService = m_webServices[j]; int id = webService.id(); m_webServicesIds.insert(id, j); @@ -526,16 +483,14 @@ void OptionsWindow::showWebServices() label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); ui->layoutWebServices->addWidget(label, j, 1); - if (j > 0) - { + if (j > 0) { QPushButton *buttonMoveUp = new QPushButton(QIcon(":/images/icons/arrow-up.png"), QString()); mapperMoveUpWebService->setMapping(buttonMoveUp, id); connect(buttonMoveUp, SIGNAL(clicked(bool)), mapperMoveUpWebService, SLOT(map())); ui->layoutWebServices->addWidget(buttonMoveUp, j, 2); } - if (j < m_webServices.count() - 1) - { + if (j < m_webServices.count() - 1) { QPushButton *buttonMoveDown = new QPushButton(QIcon(":/images/icons/arrow-down.png"), QString()); mapperMoveDownWebService->setMapping(buttonMoveDown, id); connect(buttonMoveDown, SIGNAL(clicked(bool)), mapperMoveDownWebService, SLOT(map())); @@ -592,16 +547,16 @@ void OptionsWindow::setWebService(ReverseSearchEngine rse, const QByteArray &fav const bool isNew = rse.id() < 0; // Generate new ID for new web services - if (isNew) - { + if (isNew) { int maxOrder = 0; int maxId = 0; - for (const ReverseSearchEngine &ws : qAsConst(m_webServices)) - { - if (ws.id() > maxId) + for (const ReverseSearchEngine &ws : qAsConst(m_webServices)) { + if (ws.id() > maxId) { maxId = ws.id(); - if (ws.order() > maxOrder) + } + if (ws.order() > maxOrder) { maxOrder = ws.order(); + } } rse.setId(maxId + 1); @@ -609,22 +564,21 @@ void OptionsWindow::setWebService(ReverseSearchEngine rse, const QByteArray &fav } // Write icon information to disk - if (!favicon.isEmpty()) - { + if (!favicon.isEmpty()) { QString faviconPath = savePath("webservices/") + QString::number(rse.id()) + ".ico"; QFile f(faviconPath); - if (f.open(QFile::WriteOnly)) - { + if (f.open(QFile::WriteOnly)) { f.write(favicon); f.close(); } rse = ReverseSearchEngine(rse.id(), faviconPath, rse.name(), rse.tpl(), rse.order()); } - if (isNew) - { m_webServices.append(rse); } - else - { m_webServices[m_webServicesIds[rse.id()]] = rse; } + if (isNew) { + m_webServices.append(rse); + } else { + m_webServices[m_webServicesIds[rse.id()]] = rse; + } showWebServices(); } @@ -632,8 +586,9 @@ void OptionsWindow::setWebService(ReverseSearchEngine rse, const QByteArray &fav void OptionsWindow::moveUpWebService(int id) { const int i = m_webServicesIds[id]; - if (i == 0) + if (i == 0) { return; + } swapWebServices(i, i - 1); } @@ -641,8 +596,9 @@ void OptionsWindow::moveUpWebService(int id) void OptionsWindow::moveDownWebService(int id) { const int i = m_webServicesIds[id]; - if (i == m_webServicesIds.count() - 1) + if (i == m_webServicesIds.count() - 1) { return; + } swapWebServices(i, i + 1); } @@ -658,8 +614,9 @@ void OptionsWindow::swapWebServices(int a, int b) // Re-order web services std::sort(m_webServices.begin(), m_webServices.end(), sortByOrder); m_webServicesIds.clear(); - for (int i = 0; i < m_webServices.count(); ++i) + for (int i = 0; i < m_webServices.count(); ++i) { m_webServicesIds.insert(m_webServices[i].id(), i); + } showWebServices(); } @@ -672,13 +629,12 @@ void OptionsWindow::setColor(QLineEdit *lineEdit, bool button) ? QColorDialog::getColor(QColor(text), this, tr("Choose a color")) : QColor(text); - if (color.isValid()) - { + if (color.isValid()) { lineEdit->setText(button ? color.name() : text); lineEdit->setStyleSheet("color:" + color.name()); + } else if (!button) { + lineEdit->setStyleSheet("color:#000000"); } - else if (!button) - { lineEdit->setStyleSheet("color:#000000"); } } void OptionsWindow::setFont(QLineEdit *lineEdit) @@ -686,8 +642,9 @@ void OptionsWindow::setFont(QLineEdit *lineEdit) bool ok = false; const QFont police = QFontDialog::getFont(&ok, lineEdit->font(), this, tr("Choose a font")); - if (ok) - { lineEdit->setFont(police); } + if (ok) { + lineEdit->setFont(police); + } } void OptionsWindow::on_lineColoringArtists_textChanged() @@ -776,18 +733,17 @@ void OptionsWindow::on_buttonImageBackgroundColor_clicked() void treeWidgetRec(int depth, bool &found, int &index, QTreeWidgetItem *current, QTreeWidgetItem *sel) { - if (current == sel) - { + if (current == sel) { found = true; return; } index++; - for (int i = 0; i < current->childCount(); ++i) - { + for (int i = 0; i < current->childCount(); ++i) { treeWidgetRec(depth + 1, found, index, current->child(i), sel); - if (found) + if (found) { break; + } } } @@ -798,15 +754,16 @@ void OptionsWindow::updateContainer(QTreeWidgetItem *current, QTreeWidgetItem *p bool found = false; int index = 0; - for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) - { + for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) { treeWidgetRec(0, found, index, ui->treeWidget->topLevelItem(i), current); - if (found) + if (found) { break; + } } - if (found) + if (found) { ui->stackedWidget->setCurrentIndex(index); + } } void OptionsWindow::save() @@ -816,6 +773,7 @@ void OptionsWindow::save() settings->setValue("whitelistedtags", ui->lineWhitelist->text()); settings->setValue("ignoredtags", ui->lineIgnored->text()); settings->setValue("add", ui->lineAdd->text()); + settings->setValue("globalPostFilter", ui->lineGlobalPostFilter->text()); QStringList wl = QStringList() << "never" << "image" << "page"; settings->setValue("whitelist_download", wl.at(ui->comboWhitelist->currentIndex())); @@ -837,20 +795,17 @@ void OptionsWindow::save() settings->setValue("getunloadedpages", ui->checkGetUnloadedPages->isChecked()); settings->setValue("invertToggle", ui->checkInvertToggle->isChecked()); settings->setValue("confirm_close", ui->checkConfirmClose->isChecked()); - QList checkForUpdates = QList() << 0 << 24*60*60 << 7*24*60*60 << 30*24*60*60 << -1; + settings->setValue("send_usage_data", ui->checkSendUsageData->isChecked()); + QList checkForUpdates = QList() << 0 << 24 * 60 * 60 << 7 * 24 * 60 * 60 << 30 * 24 * 60 * 60 << -1; settings->setValue("check_for_updates", checkForUpdates.at(ui->comboCheckForUpdates->currentIndex())); settings->beginGroup("Filenames"); - for (int i = 0; i < m_filenamesConditions.size(); i++) - { - if (!m_filenamesConditions.at(i)->text().isEmpty()) - { + for (int i = 0; i < m_filenamesConditions.size(); i++) { + if (!m_filenamesConditions.at(i)->text().isEmpty()) { settings->setValue(QString::number(i) + "_cond", m_filenamesConditions.at(i)->text()); settings->setValue(QString::number(i) + "_fn", m_filenamesFilenames.at(i)->text()); settings->setValue(QString::number(i) + "_dir", m_filenamesFolders.at(i)->text()); - } - else - { + } else { settings->remove(QString::number(i) + "_cond"); settings->remove(QString::number(i) + "_fn"); settings->remove(QString::number(i) + "_dir"); @@ -865,8 +820,7 @@ void OptionsWindow::save() settings->setValue("preloadAllTabs", ui->checkPreloadAllTabs->isChecked()); QStringList ftypes = QStringList() << "ind" << "in" << "id" << "nd" << "i" << "n" << "d"; - if (settings->value("favorites_display", "ind").toString() != ftypes.at(ui->comboFavoritesDisplay->currentIndex())) - { + if (settings->value("favorites_display", "ind").toString() != ftypes.at(ui->comboFavoritesDisplay->currentIndex())) { settings->setValue("favorites_display", ftypes.at(ui->comboFavoritesDisplay->currentIndex())); m_profile->emitFavorite(); } @@ -878,8 +832,9 @@ void OptionsWindow::save() // Blacklist Blacklist blacklist; - for (const QString &tags : ui->textBlacklist->toPlainText().split("\n", QString::SkipEmptyParts)) - { blacklist.add(tags.trimmed().split(' ', QString::SkipEmptyParts)); } + for (const QString &tags : ui->textBlacklist->toPlainText().split("\n", QString::SkipEmptyParts)) { + blacklist.add(tags.trimmed().split(' ', QString::SkipEmptyParts)); + } m_profile->setBlacklistedTags(blacklist); settings->setValue("downloadblacklist", ui->checkDownloadBlacklisted->isChecked()); @@ -913,34 +868,32 @@ void OptionsWindow::save() settings->setValue("path", folder); settings->setValue("path_real", folder); QDir pth = QDir(folder); - if (!pth.exists()) - { + if (!pth.exists()) { QString op; - while (!pth.exists() && pth.path() != op) - { + while (!pth.exists() && pth.path() != op) { op = pth.path(); pth.setPath(pth.path().remove(QRegularExpression("/([^/]+)$"))); } - if (pth.path() == op) - { error(this, tr("An error occured creating the save folder.")); } - else - { pth.mkpath(folder); } + if (pth.path() == op) { + error(this, tr("An error occured creating the save folder.")); + } else { + pth.mkpath(folder); + } } folder = fixFilename("", ui->lineFolderFavorites->text()); settings->setValue("path_favorites", folder); pth = QDir(folder); - if (!pth.exists()) - { + if (!pth.exists()) { QString op; - while (!pth.exists() && pth.path() != op) - { + while (!pth.exists() && pth.path() != op) { op = pth.path(); pth.setPath(pth.path().remove(QRegularExpression("/([^/]+)$"))); } - if (pth.path() == op) - { error(this, tr("An error occured creating the favorites save folder.")); } - else - { pth.mkpath(folder); } + if (pth.path() == op) { + error(this, tr("An error occured creating the favorites save folder.")); + } else { + pth.mkpath(folder); + } } QStringList md5Duplicates = QStringList() << "save" << "copy" << "move" << "link" << "ignore"; settings->setValue("md5Duplicates", md5Duplicates.at(ui->comboMd5Duplicates->currentIndex())); @@ -952,96 +905,23 @@ void OptionsWindow::save() settings->setValue("filename_real", ui->lineFilename->text()); settings->setValue("filename_favorites", ui->lineFavorites->text()); - settings->setValue("artist_empty", ui->lineArtistsIfNone->text()); - settings->setValue("artist_useall", ui->radioArtistsKeepAll->isChecked()); - QString artistMultiple; - if (ui->radioArtistsKeepAll->isChecked()) { artistMultiple = "keepAll"; } - else if (ui->radioArtistsKeepN->isChecked()) { artistMultiple = "keepN"; } - else if (ui->radioArtistsKeepNThenAdd->isChecked()) { artistMultiple = "keepNThenAdd"; } - else if (ui->radioArtistsReplaceAll->isChecked()) { artistMultiple = "replaceAll"; } - else if (ui->radioArtistsMultiple->isChecked()) { artistMultiple = "multiple"; } - settings->setValue("artist_multiple", artistMultiple); - settings->setValue("artist_multiple_limit", ui->spinArtistsMoreThanN->value()); - settings->setValue("artist_multiple_keepN", ui->spinArtistsKeepN->value()); - settings->setValue("artist_multiple_keepNThenAdd_keep", ui->spinArtistsKeepNThenAdd->value()); - settings->setValue("artist_multiple_keepNThenAdd_add", ui->lineArtistsKeepNThenAdd->text()); - settings->setValue("artist_sep", ui->lineArtistsSeparator->text()); - settings->setValue("artist_value", ui->lineArtistsReplaceAll->text()); - - settings->setValue("copyright_empty", ui->lineCopyrightsIfNone->text()); - settings->setValue("copyright_useshorter", ui->checkCopyrightsUseShorter->isChecked()); - QString copyrightMultiple; - if (ui->radioCopyrightsKeepAll->isChecked()) { copyrightMultiple = "keepAll"; } - else if (ui->radioCopyrightsKeepN->isChecked()) { copyrightMultiple = "keepN"; } - else if (ui->radioCopyrightsKeepNThenAdd->isChecked()) { copyrightMultiple = "keepNThenAdd"; } - else if (ui->radioCopyrightsReplaceAll->isChecked()) { copyrightMultiple = "replaceAll"; } - else if (ui->radioCopyrightsMultiple->isChecked()) { copyrightMultiple = "multiple"; } - settings->setValue("copyright_multiple", copyrightMultiple); - settings->setValue("copyright_multiple_limit", ui->spinCopyrightsMoreThanN->value()); - settings->setValue("copyright_multiple_keepN", ui->spinCopyrightsKeepN->value()); - settings->setValue("copyright_multiple_keepNThenAdd_keep", ui->spinCopyrightsKeepNThenAdd->value()); - settings->setValue("copyright_multiple_keepNThenAdd_add", ui->lineCopyrightsKeepNThenAdd->text()); - settings->setValue("copyright_sep", ui->lineCopyrightsSeparator->text()); - settings->setValue("copyright_value", ui->lineCopyrightsReplaceAll->text()); - - settings->setValue("character_empty", ui->lineCharactersIfNone->text()); - QString characterMultiple; - if (ui->radioCharactersKeepAll->isChecked()) { characterMultiple = "keepAll"; } - else if (ui->radioCharactersKeepN->isChecked()) { characterMultiple = "keepN"; } - else if (ui->radioCharactersKeepNThenAdd->isChecked()) { characterMultiple = "keepNThenAdd"; } - else if (ui->radioCharactersReplaceAll->isChecked()) { characterMultiple = "replaceAll"; } - else if (ui->radioCharactersMultiple->isChecked()) { characterMultiple = "multiple"; } - settings->setValue("character_multiple", characterMultiple); - settings->setValue("character_multiple_limit", ui->spinCharactersMoreThanN->value()); - settings->setValue("character_multiple_keepN", ui->spinCharactersKeepN->value()); - settings->setValue("character_multiple_keepNThenAdd_keep", ui->spinCharactersKeepNThenAdd->value()); - settings->setValue("character_multiple_keepNThenAdd_add", ui->lineCharactersKeepNThenAdd->text()); - settings->setValue("character_sep", ui->lineCharactersSeparator->text()); - settings->setValue("character_value", ui->lineCharactersReplaceAll->text()); - - settings->setValue("species_empty", ui->lineSpeciesIfNone->text()); - QString speciesMultiple; - if (ui->radioSpeciesKeepAll->isChecked()) { speciesMultiple = "keepAll"; } - else if (ui->radioSpeciesKeepN->isChecked()) { speciesMultiple = "keepN"; } - else if (ui->radioSpeciesKeepNThenAdd->isChecked()) { speciesMultiple = "keepNThenAdd"; } - else if (ui->radioSpeciesReplaceAll->isChecked()) { speciesMultiple = "replaceAll"; } - else if (ui->radioSpeciesMultiple->isChecked()) { speciesMultiple = "multiple"; } - settings->setValue("species_multiple", speciesMultiple); - settings->setValue("species_multiple_limit", ui->spinSpeciesMoreThanN->value()); - settings->setValue("species_multiple_keepN", ui->spinSpeciesKeepN->value()); - settings->setValue("species_multiple_keepNThenAdd_keep", ui->spinSpeciesKeepNThenAdd->value()); - settings->setValue("species_multiple_keepNThenAdd_add", ui->lineSpeciesKeepNThenAdd->text()); - settings->setValue("species_sep", ui->lineSpeciesSeparator->text()); - settings->setValue("species_value", ui->lineSpeciesReplaceAll->text()); - - settings->setValue("meta_empty", ui->lineMetasIfNone->text()); - QString metaMultiple; - if (ui->radioMetasKeepAll->isChecked()) { metaMultiple = "keepAll"; } - else if (ui->radioMetasKeepN->isChecked()) { metaMultiple = "keepN"; } - else if (ui->radioMetasKeepNThenAdd->isChecked()) { metaMultiple = "keepNThenAdd"; } - else if (ui->radioMetasReplaceAll->isChecked()) { metaMultiple = "replaceAll"; } - else if (ui->radioMetasMultiple->isChecked()) { metaMultiple = "multiple"; } - settings->setValue("meta_multiple", metaMultiple); - settings->setValue("meta_multiple_limit", ui->spinMetasMoreThanN->value()); - settings->setValue("meta_multiple_keepN", ui->spinMetasKeepN->value()); - settings->setValue("meta_multiple_keepNThenAdd_keep", ui->spinMetasKeepNThenAdd->value()); - settings->setValue("meta_multiple_keepNThenAdd_add", ui->lineMetasKeepNThenAdd->text()); - settings->setValue("meta_sep", ui->lineMetasSeparator->text()); - settings->setValue("meta_value", ui->lineMetasReplaceAll->text()); + for (TokenSettingsWidget *tokenSettings : m_tokenSettings) { + tokenSettings->save(); + } settings->setValue("limit", ui->spinLimit->value()); settings->setValue("simultaneous", ui->spinSimultaneous->value()); settings->beginGroup("Customs"); settings->remove(""); - for (int j = 0; j < m_customNames.size(); j++) - { settings->setValue(m_customNames[j]->text(), m_customTags[j]->text()); } + for (int j = 0; j < m_customNames.size(); j++) { + settings->setValue(m_customNames[j]->text(), m_customTags[j]->text()); + } settings->endGroup(); settings->endGroup(); // Web services settings->beginGroup("WebServices"); - for (const ReverseSearchEngine &webService : qAsConst(m_webServices)) - { + for (const ReverseSearchEngine &webService : qAsConst(m_webServices)) { settings->beginGroup(QString::number(webService.id())); settings->setValue("name", webService.name()); settings->setValue("url", webService.tpl()); @@ -1053,8 +933,9 @@ void OptionsWindow::save() // Themes const QString theme = ui->comboTheme->currentText(); ThemeLoader themeLoader(savePath("themes/", true)); - if (themeLoader.setTheme(theme)) - { settings->setValue("theme", theme); } + if (themeLoader.setTheme(theme)) { + settings->setValue("theme", theme); + } settings->setValue("Zoom/singleWindow", ui->checkSingleDetailsWindow->isChecked()); QStringList positions = QStringList() << "top" << "left" << "auto"; @@ -1066,7 +947,7 @@ void OptionsWindow::save() settings->setValue("imageCloseMiddleClick", ui->checkImageCloseMiddleClick->isChecked()); settings->setValue("imageNavigateScroll", ui->checkImageNavigateScroll->isChecked()); settings->setValue("Zoom/showTagCount", ui->checkZoomShowTagCount->isChecked()); - //settings->setValue("Zoom/viewSamples", ui->checkZoomViewSamples->isChecked()); + settings->setValue("Zoom/viewSamples", ui->checkZoomViewSamples->isChecked()); QStringList imageTagOrder = QStringList() << "type" << "name" << "count"; settings->setValue("Zoom/tagOrder", imageTagOrder.at(ui->comboImageTagOrder->currentIndex())); QStringList positionsV = QStringList() << "top" << "center" << "bottom"; @@ -1148,13 +1029,11 @@ void OptionsWindow::save() settings->endGroup(); settings->endGroup(); - if (settings->value("Proxy/use", false).toBool()) - { + if (settings->value("Proxy/use", false).toBool()) { const bool useSystem = settings->value("Proxy/useSystem", false).toBool(); QNetworkProxyFactory::setUseSystemConfiguration(useSystem); - if (!useSystem) - { + if (!useSystem) { const QNetworkProxy::ProxyType type = settings->value("Proxy/type", "http") == "http" ? QNetworkProxy::HttpProxy : QNetworkProxy::Socks5Proxy; @@ -1167,19 +1046,16 @@ void OptionsWindow::save() ); QNetworkProxy::setApplicationProxy(proxy); log(QStringLiteral("Enabling application proxy on host \"%1\" and port %2.").arg(settings->value("Proxy/hostName").toString()).arg(settings->value("Proxy/port").toInt())); + } else { + log(QStringLiteral("Enabling system-wide proxy.")); } - else - { log(QStringLiteral("Enabling system-wide proxy.")); } - } - else if (QNetworkProxy::applicationProxy().type() != QNetworkProxy::NoProxy) - { + } else if (QNetworkProxy::applicationProxy().type() != QNetworkProxy::NoProxy) { QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); log(QStringLiteral("Disabling application proxy.")); } const QString lang = ui->comboLanguages->currentData().toString(); - if (settings->value("language", "English").toString() != lang) - { + if (settings->value("language", "English").toString() != lang) { settings->setValue("language", lang); emit languageChanged(lang); } diff --git a/gui/src/settings/options-window.h b/gui/src/settings/options-window.h index 58d93dde7..b02360c7e 100644 --- a/gui/src/settings/options-window.h +++ b/gui/src/settings/options-window.h @@ -14,6 +14,7 @@ namespace Ui class Profile; +class TokenSettingsWidget; class OptionsWindow : public QDialog { @@ -106,6 +107,7 @@ class OptionsWindow : public QDialog QList m_webServices; QMap m_webServicesIds; QList m_customNames, m_customTags, m_filenamesConditions, m_filenamesFilenames, m_filenamesFolders; + QList m_tokenSettings; }; #endif // OPTIONS_WINDOW_H diff --git a/gui/src/settings/options-window.ui b/gui/src/settings/options-window.ui index be1f223ff..f220b071e 100644 --- a/gui/src/settings/options-window.ui +++ b/gui/src/settings/options-window.ui @@ -85,32 +85,12 @@ - Artist tags - - - - - Copyright tags - - - - - Character tags - - - - - Species tags - - - - - Meta tags + Custom token - Custom token + Tags @@ -351,23 +331,40 @@ - + - <i>These tags will be automatically added to every search.</i> + <i>These tags and post-filters will be automatically added to every search.</i> true - + Ask for confirmation before closing the window + + + + Post-filters + + + + + + + + + + Send anonymous usage data + + + label comboLanguages @@ -388,6 +385,9 @@ checkConfirmClose labelCheckForUpdates comboCheckForUpdates + labelGlobalPostFilter + lineGlobalPostFilter + checkSendUsageData @@ -995,8 +995,8 @@ 0 0 - 100 - 30 + 65 + 16 @@ -1008,634 +1008,69 @@ 0 - - - 6 - - - 0 - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - <i>Each time an image is saved, its information can be added to a separate text file for later processing or for organization purposes.</i> - - - true - - - - - - - - - - Add a separate log file - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - - - If empty - - - - - - - - - - - - - - Separator - - - - - - - - - - - - - - If more than n tags - - - - - - - 100 - - - 1 - - - - - - - Keep n tags, then add - - - - - - - Replace all tags by - - - true - - - - - - - - - - - - - - Keep n tags - - - - - - - - - - - - - - - (+ %count%) - - - - - - - - - Keep all tags - - - - - - - One file per tag - - - - - - - - - - - If empty - - - - - - - - - - - - - - Separator - - - - - - - - - - - - - - If more than n tags - - - - - - - 100 - - - 1 - - - - - - - Keep n tags - - - - - - - - - - Keep n tags, then add - - - - - - - - - - - - (+ %count%) - - - - - - - - - Replace all tags by - - - true - - - - - - - - - - - - - - Use shortest if possible - - - true - - - - - - - Keep all tags - - - - - - - One file per tag - - - - - - - - - - - If empty - - - - - - - - - - - - - - Separator - - - - - - - - - - - - - - If more than n tags - - - - - - - 100 - - - 1 - - - - - - - Keep n tags - - - - - - - - - - Keep n tags, then add - - - - - - - - - - - - (+ %count%) - - - - - - - - - Replace all tags by - - - true - - - - - - - - - - - - - - One file per tag - - - - - - - Keep all tags - - - - - - - - - - - If empty - - - - - - - - - - - - - - Separator - - - - - - - - - - - - - - If more than n tags - - - - - - - 100 - - - 1 - - - - - - - Keep n tags - - - - - - - - - - Keep n tags, then add - - - - - - - - - - - - (+ %count%) - - - - - - - - - Replace all tags by - - - true - - - - - - - - - - - - - - Keep all tags - - - - - - - One file per tag - - - - - - - - - - - If empty - - - - - - - - - - - - - - Separator - - - - - - - - - - - - - - If more than n tags - - - - - - - 100 - - - 1 - - - - - - - Keep all tags - + + + 6 + + + 0 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - - + + + + + + - Keep n tags + <i>Each time an image is saved, its information can be added to a separate text file for later processing or for organization purposes.</i> - - - - - - - - - Keep n tags, then add + + true - - - - - - - - - (+ %count%) - - - - + + - - + + - Replace all tags by - - - true + Add a separate log file - - - - + + + + Qt::Vertical - - - - - - One file per tag + + + 0 + 0 + - + @@ -1650,6 +1085,7 @@ + @@ -1984,6 +1420,13 @@ + + + + Use a single image window + + + @@ -2057,21 +1500,21 @@ - + Show tag count - + Tag order - + @@ -2090,14 +1533,14 @@ - + Image position - + @@ -2145,14 +1588,14 @@ - + Animation position - + @@ -2200,14 +1643,14 @@ - + Video position - + @@ -2255,14 +1698,14 @@ - + Background color - + @@ -2276,10 +1719,10 @@ - - + + - Use a single image window + Use image samples @@ -3289,11 +2732,6 @@ buttonFilenamePlus lineFavorites buttonFavoritesPlus - lineArtistsIfNone - lineArtistsSeparator - lineCopyrightsIfNone - lineCharactersIfNone - lineCharactersSeparator spinMainMargins spinServerBorders lineBorderColor @@ -3361,146 +2799,18 @@ - spinCharactersKeepNThenAdd - valueChanged(int) - radioCharactersKeepNThenAdd - click() - - - 528 - 165 - - - 408 - 161 - - - - - spinCopyrightsKeepNThenAdd - valueChanged(int) - radioCopyrightsKeepNThenAdd - click() - - - 528 - 165 - - - 408 - 161 - - - - - spinArtistsKeepNThenAdd - valueChanged(int) - radioArtistsKeepNThenAdd - click() - - - 528 - 165 - - - 408 - 161 - - - - - lineCopyrightsReplaceAll - textEdited(QString) - radioCopyrightsReplaceAll - click() - - - 647 - 192 - - - 391 - 189 - - - - - lineCharactersKeepNThenAdd - textEdited(QString) - radioCharactersKeepNThenAdd - click() - - - 646 - 165 - - - 408 - 161 - - - - - spinCopyrightsKeepN - valueChanged(int) - radioCopyrightsKeepN - click() - - - 647 - 138 - - - 358 - 135 - - - - - spinArtistsKeepN - valueChanged(int) - radioArtistsKeepN - click() - - - 647 - 138 - - - 358 - 135 - - - - - lineCharactersReplaceAll - textEdited(QString) - radioCharactersReplaceAll - click() - - - 647 - 192 - - - 391 - 189 - - - - - spinCharactersKeepN - valueChanged(int) - radioCharactersKeepN - click() + checkMonitoringEnableTray + toggled(bool) + checkMonitoringCloseToTray + setEnabled(bool) - 647 - 138 + 350 + 67 - 358 - 135 + 350 + 113 @@ -3520,70 +2830,6 @@ - - lineArtistsReplaceAll - textEdited(QString) - radioArtistsReplaceAll - click() - - - 647 - 192 - - - 391 - 189 - - - - - checkProxyUse - toggled(bool) - widgetProxy - setEnabled(bool) - - - 360 - 20 - - - 360 - 43 - - - - - lineCopyrightsKeepNThenAdd - textEdited(QString) - radioCopyrightsKeepNThenAdd - click() - - - 646 - 165 - - - 408 - 161 - - - - - lineArtistsKeepNThenAdd - textEdited(QString) - radioArtistsKeepNThenAdd - click() - - - 646 - 165 - - - 408 - 161 - - - buttonAddLogFile clicked() @@ -3601,18 +2847,18 @@ - buttonAddWebService - clicked() - OptionsWindow - addWebService() + checkProxyUse + toggled(bool) + widgetProxy + setEnabled(bool) 360 - 26 + 20 - 661 - 119 + 360 + 43 @@ -3633,18 +2879,18 @@ - checkMonitoringEnableTray - toggled(bool) - checkMonitoringCloseToTray - setEnabled(bool) + buttonAddWebService + clicked() + OptionsWindow + addWebService() - 350 - 67 + 360 + 26 - 350 - 113 + 661 + 119 diff --git a/gui/src/settings/start-window.cpp b/gui/src/settings/start-window.cpp index 096e1374f..355bcbd90 100644 --- a/gui/src/settings/start-window.cpp +++ b/gui/src/settings/start-window.cpp @@ -21,15 +21,17 @@ StartWindow::StartWindow(Profile *profile, QWidget *parent) // Language LanguageLoader languageLoader(savePath("languages/", true)); QMap languages = languageLoader.getAllLanguages(); - for (auto it = languages.constBegin(); it != languages.constEnd(); ++it) - { ui->comboLanguage->addItem(it.value(), it.key()); } + for (auto it = languages.constBegin(); it != languages.constEnd(); ++it) { + ui->comboLanguage->addItem(it.value(), it.key()); + } ui->comboLanguage->setCurrentText("English"); // Sources QStringList sources = profile->getSites().keys(); ui->comboSource->addItems(sources); - if (sources.contains("danbooru.donmai.us")) - { ui->comboSource->setCurrentText("danbooru.donmai.us"); } + if (sources.contains("danbooru.donmai.us")) { + ui->comboSource->setCurrentText("danbooru.donmai.us"); + } // Default values QDir desktop(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); @@ -50,8 +52,9 @@ StartWindow::~StartWindow() void StartWindow::on_buttonFolder_clicked() { QString folder = QFileDialog::getExistingDirectory(this, tr("Choose a save folder"), ui->lineFolder->text()); - if (!folder.isEmpty()) - { ui->lineFolder->setText(folder); } + if (!folder.isEmpty()) { + ui->lineFolder->setText(folder); + } } void StartWindow::on_buttonFilenamePlus_clicked() { @@ -76,25 +79,23 @@ void StartWindow::save() settings->setValue("path", ui->lineFolder->text()); settings->setValue("path_real", ui->lineFolder->text()); QDir pth = QDir(ui->lineFolder->text()); - if (!pth.exists()) - { + if (!pth.exists()) { QString op; - while (!pth.exists() && pth.path() != op) - { + while (!pth.exists() && pth.path() != op) { op = pth.path(); pth.setPath(pth.path().remove(QRegularExpression("/([^/]+)$"))); } - if (pth.path() == op) - { error(this, tr("An error occurred creating the save folder.")); } - else - { pth.mkpath(ui->lineFolder->text()); } + if (pth.path() == op) { + error(this, tr("An error occurred creating the save folder.")); + } else { + pth.mkpath(ui->lineFolder->text()); + } } settings->endGroup(); // Language QString lang = ui->comboLanguage->currentData().toString(); - if (settings->value("language", "English").toString() != lang) - { + if (settings->value("language", "English").toString() != lang) { settings->setValue("language", lang); emit languageChanged(lang); } diff --git a/gui/src/settings/token-settings-widget.cpp b/gui/src/settings/token-settings-widget.cpp new file mode 100644 index 000000000..67fd72f9e --- /dev/null +++ b/gui/src/settings/token-settings-widget.cpp @@ -0,0 +1,76 @@ +#include "token-settings-widget.h" +#include +#include "ui_token-settings-widget.h" + +TokenSettingsWidget::TokenSettingsWidget(QSettings *settings, QString name, bool enableShorter, const QString &defaultEmpty, const QString &defaultMultiple, QWidget *parent) + : QWidget(parent), ui(new Ui::TokenSettingsWidget), m_settings(settings), m_name(std::move(name)), m_enableShorter(enableShorter) +{ + ui->setupUi(this); + + static const QStringList tagsSort { "original", "name" }; + + ui->lineIfNone->setText(m_settings->value(m_name + "_empty", defaultEmpty).toString()); + ui->comboSort->setCurrentIndex(tagsSort.indexOf(m_settings->value(m_name + "_sort", "original").toString())); + ui->spinMoreThanN->setValue(m_settings->value(m_name + "_multiple_limit", 1).toInt()); + ui->spinKeepN->setValue(m_settings->value(m_name + "_multiple_keepN", 1).toInt()); + ui->spinKeepNThenAdd->setValue(m_settings->value(m_name + "_multiple_keepNThenAdd_keep", 1).toInt()); + ui->lineKeepNThenAdd->setText(m_settings->value(m_name + "_multiple_keepNThenAdd_add", " (+ %count%)").toString()); + ui->lineSeparator->setText(m_settings->value(m_name + "_sep", "+").toString()); + ui->lineReplaceAll->setText(m_settings->value(m_name + "_value", defaultMultiple).toString()); + + const QString multiple = m_settings->value(m_name + "_multiple", "keepAll").toString(); + if (multiple == "keepAll") { + ui->radioKeepAll->setChecked(true); + } else if (multiple == "keepN") { + ui->radioKeepN->setChecked(true); + } else if (multiple == "keepNThenAdd") { + ui->radioKeepNThenAdd->setChecked(true); + } else if (multiple == "replaceAll") { + ui->radioReplaceAll->setChecked(true); + } else if (multiple == "multiple") { + ui->radioMultiple->setChecked(true); + } + + ui->checkUseShorter->setVisible(m_enableShorter); + if (m_enableShorter) { + ui->checkUseShorter->setChecked(settings->value(m_name + "_useshorter", true).toBool()); + } +} + +TokenSettingsWidget::~TokenSettingsWidget() +{ + delete ui; +} + +void TokenSettingsWidget::save() +{ + static const QStringList tagsSort { "original", "name" }; + + m_settings->setValue(m_name + "_empty", ui->lineIfNone->text()); + m_settings->setValue(m_name + "_sort", tagsSort.at(ui->comboSort->currentIndex())); + m_settings->setValue(m_name + "_useall", ui->radioKeepAll->isChecked()); + m_settings->setValue(m_name + "_multiple_limit", ui->spinMoreThanN->value()); + m_settings->setValue(m_name + "_multiple_keepN", ui->spinKeepN->value()); + m_settings->setValue(m_name + "_multiple_keepNThenAdd_keep", ui->spinKeepNThenAdd->value()); + m_settings->setValue(m_name + "_multiple_keepNThenAdd_add", ui->lineKeepNThenAdd->text()); + m_settings->setValue(m_name + "_sep", ui->lineSeparator->text()); + m_settings->setValue(m_name + "_value", ui->lineReplaceAll->text()); + + QString artistMultiple; + if (ui->radioKeepAll->isChecked()) { + artistMultiple = "keepAll"; + } else if (ui->radioKeepN->isChecked()) { + artistMultiple = "keepN"; + } else if (ui->radioKeepNThenAdd->isChecked()) { + artistMultiple = "keepNThenAdd"; + } else if (ui->radioReplaceAll->isChecked()) { + artistMultiple = "replaceAll"; + } else if (ui->radioMultiple->isChecked()) { + artistMultiple = "multiple"; + } + m_settings->setValue(m_name + "_multiple", artistMultiple); + + if (m_enableShorter) { + m_settings->setValue(m_name + "_useshorter", ui->checkUseShorter->isChecked()); + } +} diff --git a/gui/src/settings/token-settings-widget.h b/gui/src/settings/token-settings-widget.h new file mode 100644 index 000000000..d527915cb --- /dev/null +++ b/gui/src/settings/token-settings-widget.h @@ -0,0 +1,33 @@ +#ifndef TOKEN_SETTINGS_WIDGET_H +#define TOKEN_SETTINGS_WIDGET_H + +#include +#include + + +namespace Ui { + class TokenSettingsWidget; +} + + +class QSettings; + +class TokenSettingsWidget : public QWidget +{ + Q_OBJECT + + public: + explicit TokenSettingsWidget(QSettings *settings, QString name, bool enableShorter, const QString &defaultEmpty = "unknown", const QString &defaultMultiple = "multiple", QWidget *parent = nullptr); + ~TokenSettingsWidget() override; + + public slots: + void save(); + + private: + Ui::TokenSettingsWidget *ui; + QSettings *m_settings; + QString m_name; + bool m_enableShorter; +}; + +#endif // TOKEN_SETTINGS_WIDGET_H diff --git a/gui/src/settings/token-settings-widget.ui b/gui/src/settings/token-settings-widget.ui new file mode 100644 index 000000000..cf3006e2e --- /dev/null +++ b/gui/src/settings/token-settings-widget.ui @@ -0,0 +1,159 @@ + + + TokenSettingsWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + If empty + + + + + + + + + + + + + + Separator + + + + + + + + + + + + + + Sort + + + + + + + + Original + + + + + Name + + + + + + + + If more than n tags + + + + + + + 100 + + + 1 + + + + + + + Keep all tags + + + + + + + Keep n tags + + + + + + + + + + Keep n tags, then add + + + + + + + + + + + + (+ %count%) + + + + + + + + + Replace all tags by + + + true + + + + + + + + + + + + + + One file per tag + + + + + + + Use shortest if possible + + + true + + + + + + + + diff --git a/gui/src/settings/web-service-window.cpp b/gui/src/settings/web-service-window.cpp index 7a6b6af58..a8785f178 100644 --- a/gui/src/settings/web-service-window.cpp +++ b/gui/src/settings/web-service-window.cpp @@ -13,8 +13,7 @@ WebServiceWindow::WebServiceWindow(const ReverseSearchEngine *webService, QWidge m_networkAccessManager = new CustomNetworkAccessManager(this); - if (webService != nullptr) - { + if (webService != nullptr) { ui->lineName->setText(webService->name()); ui->lineUrl->setText(webService->tpl()); } @@ -41,8 +40,7 @@ void WebServiceWindow::faviconReceived() { // Check redirection QUrl redirection = m_faviconReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redirection.isEmpty()) - { + if (!redirection.isEmpty()) { m_faviconReply = m_networkAccessManager->get(QNetworkRequest(QUrl(redirection))); connect(m_faviconReply, &QNetworkReply::finished, this, &WebServiceWindow::faviconReceived); return; @@ -54,16 +52,14 @@ void WebServiceWindow::faviconReceived() void WebServiceWindow::save() { int id = -1, order = 0; - if (m_webService != nullptr) - { + if (m_webService != nullptr) { id = m_webService->id(); order = m_webService->order(); } // Save favicon contents QByteArray faviconData; - if (m_faviconReply->error() == QNetworkReply::NoError) - { + if (m_faviconReply->error() == QNetworkReply::NoError) { faviconData = m_faviconReply->readAll(); m_faviconReply->deleteLater(); } diff --git a/gui/src/sources/site-window.cpp b/gui/src/sources/site-window.cpp index bfffe9807..22dd68246 100644 --- a/gui/src/sources/site-window.cpp +++ b/gui/src/sources/site-window.cpp @@ -20,8 +20,7 @@ SiteWindow::SiteWindow(Profile *profile, QWidget *parent) ui->checkBox->setChecked(true); m_sources = profile->getSources().values(); - for (Source *source : qAsConst(m_sources)) - { + for (Source *source : qAsConst(m_sources)) { ui->comboBox->addItem(QIcon(source->getPath() + "/icon.png"), source->getName()); } } @@ -34,22 +33,22 @@ SiteWindow::~SiteWindow() void SiteWindow::accept() { m_url = ui->lineEdit->text(); - if (!m_url.startsWith("http://") && !m_url.startsWith("https://")) - { m_url.prepend("http://"); } - if (m_url.endsWith("/")) - { m_url = m_url.left(m_url.size() - 1); } + if (!m_url.startsWith("http://") && !m_url.startsWith("https://")) { + m_url.prepend("http://"); + } + if (m_url.endsWith("/")) { + m_url = m_url.left(m_url.size() - 1); + } // Check URL validity - if (!QRegularExpression(R"(^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$)").match(m_url).hasMatch()) - { + if (!QRegularExpression(R"(^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$)").match(m_url).hasMatch()) { error(this, tr("The url you entered is not valid.")); return; } ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - if (ui->checkBox->isChecked()) - { + if (ui->checkBox->isChecked()) { ui->progressBar->setValue(0); ui->progressBar->setMaximum(m_sources.count()); ui->progressBar->show(); @@ -63,10 +62,8 @@ void SiteWindow::accept() } Source *src = nullptr; - for (Source *source : qAsConst(m_sources)) - { - if (source->getName() == ui->comboBox->currentText()) - { + for (Source *source : qAsConst(m_sources)) { + if (source->getName() == ui->comboBox->currentText()) { src = source; break; } @@ -76,8 +73,7 @@ void SiteWindow::accept() void SiteWindow::finish(Source *src) { - if (src == nullptr) - { + if (src == nullptr) { error(this, tr("Unable to guess site's type. Are you sure about the url?")); ui->comboBox->setDisabled(false); ui->checkBox->setChecked(false); @@ -86,28 +82,30 @@ void SiteWindow::finish(Source *src) return; } - if (ui->checkBox->isChecked()) - { ui->progressBar->hide(); } + if (ui->checkBox->isChecked()) { + ui->progressBar->hide(); + } // Remove unnecessary prefix bool ssl = false; - if (m_url.startsWith("http://")) - { m_url = m_url.mid(7); } - else if (m_url.startsWith("https://")) - { + if (m_url.startsWith("http://")) { + m_url = m_url.mid(7); + } else if (m_url.startsWith("https://")) { m_url = m_url.mid(8); ssl = true; } - if (m_url.endsWith('/')) - { m_url = m_url.left(m_url.length() - 1); } + if (m_url.endsWith('/')) { + m_url = m_url.left(m_url.length() - 1); + } Site *site = new Site(m_url, src); m_profile->addSite(site); // If the user wrote "https://" in the URL, we enable SSL for this site - if (ssl) - { site->setSetting("ssl", true, false); } + if (ssl) { + site->setSetting("ssl", true, false); + } // Save new sites QFile f(src->getPath() + "/sites.txt"); diff --git a/gui/src/sources/sources-settings-window.cpp b/gui/src/sources/sources-settings-window.cpp index e9775a724..01bda0376 100644 --- a/gui/src/sources/sources-settings-window.cpp +++ b/gui/src/sources/sources-settings-window.cpp @@ -1,11 +1,16 @@ #include "sources/sources-settings-window.h" #include #include +#include #include #include #include #include +#include "auth/auth-field.h" +#include "auth/auth-hash-field.h" +#include "auth/field-auth.h" #include "functions.h" +#include "mixed-settings.h" #include "models/api/api.h" #include "models/profile.h" #include "models/source.h" @@ -22,6 +27,15 @@ void setSource(QComboBox *combo, const QStringList &opts, const QStringList &val combo->setCurrentIndex(qMax(0, vals.indexOf(local))); } +QLineEdit *createLineEdit(QWidget *parent, QString value, bool isPassword) +{ + auto le = new QLineEdit(std::move(value), parent); + if (isPassword) { + le->setEchoMode(QLineEdit::Password); + } + return le; +} + SourcesSettingsWindow::SourcesSettingsWindow(Profile *profile, Site *site, QWidget *parent) : QDialog(parent), ui(new Ui::SourcesSettingsWindow), m_site(site), m_globalSettings(profile->getSettings()) { @@ -41,7 +55,6 @@ SourcesSettingsWindow::SourcesSettingsWindow(Profile *profile, Site *site, QWidg ui->checkSsl->setChecked(site->setting("ssl", false).toBool()); // Download settings - ui->spinImagesPerPage->setValue(site->setting("download/imagesperpage", 200).toInt()); ui->spinSimultaneousDownloads->setValue(site->setting("download/simultaneous", 10).toInt()); ui->spinThrottleDetails->setValue(site->setting("download/throttle_details", 0).toInt()); ui->spinThrottleImage->setValue(site->setting("download/throttle_image", 0).toInt()); @@ -54,8 +67,7 @@ SourcesSettingsWindow::SourcesSettingsWindow(Profile *profile, Site *site, QWidg static const QStringList defs = QStringList() << "xml" << "json" << "regex" << "rss"; QStringList sources = QStringList() << ""; QStringList opts = QStringList() << ""; - for (Api *api : site->getApis()) - { + for (Api *api : site->getSource()->getApis()) { const QString name = api->getName().toLower(); sources.append(name == "html" ? "regex" : name); opts.append(api->getName()); @@ -65,45 +77,74 @@ SourcesSettingsWindow::SourcesSettingsWindow(Profile *profile, Site *site, QWidg setSource(ui->comboSources3, opts, sources, defs, site, m_globalSettings, 2); setSource(ui->comboSources4, opts, sources, defs, site, m_globalSettings, 3); - // Credentials - ui->lineAuthPseudo->setText(site->setting("auth/pseudo", "").toString()); - ui->lineAuthPassword->setText(site->setting("auth/password", "").toString()); - // Login - static const QStringList types = QStringList() << "url" << "get" << "post" << "oauth1" << "oauth2"; - const QString defaultType = site->setting("login/parameter", true).toBool() ? "url" : site->setting("login/method", "post").toString(); - const QString type = site->setting("login/type", defaultType).toString(); - ui->comboLoginType->setCurrentIndex(types.indexOf(type)); - ui->lineLoginGetUrl->setText(site->setting("login/get/url", type != "get" ? "" : site->setting("login/url", "").toString()).toString()); - ui->lineLoginGetPseudo->setText(site->setting("login/get/pseudo", type != "get" ? "" : site->setting("login/pseudo", "").toString()).toString()); - ui->lineLoginGetPassword->setText(site->setting("login/get/password", type != "get" ? "" : site->setting("login/password", "").toString()).toString()); - ui->lineLoginGetCookie->setText(site->setting("login/get/cookie", type != "get" ? "" : site->setting("login/cookie", "").toString()).toString()); - ui->lineLoginPostUrl->setText(site->setting("login/post/url", type != "post" ? "" : site->setting("login/url", "").toString()).toString()); - ui->lineLoginPostPseudo->setText(site->setting("login/post/pseudo", type != "post" ? "" : site->setting("login/pseudo", "").toString()).toString()); - ui->lineLoginPostPassword->setText(site->setting("login/post/password", type != "post" ? "" : site->setting("login/password", "").toString()).toString()); - ui->lineLoginPostCookie->setText(site->setting("login/post/cookie", type != "post" ? "" : site->setting("login/cookie", "").toString()).toString()); - ui->lineLoginOAuth1RequestTokenUrl->setText(site->setting("login/oauth1/requestTokenUrl", "").toString()); - ui->lineLoginOAuth1AuthorizeUrl->setText(site->setting("login/oauth1/authorizeUrl", "").toString()); - ui->lineLoginOAuth1AccessTokenUrl->setText(site->setting("login/oauth1/accessTokenUrl", "").toString()); - ui->lineLoginOAuth2RequestUrl->setText(site->setting("login/oauth2/requestUrl", "").toString()); - ui->lineLoginOAuth2TokenUrl->setText(site->setting("login/oauth2/tokenUrl", "").toString()); - ui->lineLoginOAuth2RefreshTokenUrl->setText(site->setting("login/oauth2/refreshTokenUrl", "").toString()); - ui->lineLoginOAuth2Scope->setText(site->setting("login/oauth2/scope", "").toString()); - ui->spinLoginMaxPage->setValue(site->setting("login/maxPage", 0).toInt()); - - // Hide hash if unncessary - if (site->getApis().first()->value("PasswordSalt").isEmpty()) - { ui->buttonAuthHash->hide(); } - else - { ui->lineAuthPassword->setEchoMode(QLineEdit::Normal); } + const QString loginType = site->setting("login/type", "url").toString(); + static QMap typeNames { + { "url", tr("Through URL") }, + { "get", tr("GET") }, + { "post", tr("POST") }, + { "oauth1", tr("OAuth 1") }, + { "oauth2", tr("OAuth 2") } + }; + static QMap fieldLabels { + { "pseudo", tr("Username") }, + { "password", tr("Password") }, + { "apiKey", tr("API key") } + }; + QStringList types; + QMultiMap fields; + auto auths = m_site->getSource()->getAuths(); + for (auto it = auths.constBegin(); it != auths.constEnd(); ++it) { + const QString type = it.value()->type(); + ui->comboLoginType->addItem(typeNames.contains(type) ? typeNames[type] : type, type); + if (type == loginType) { + ui->comboLoginType->setCurrentIndex(ui->comboLoginType->count() - 1); + } + + // Build credential fields + QWidget *credentialsWidget = new QWidget(this); + QFormLayout *formLayout = new QFormLayout; + formLayout->setContentsMargins(0, 0, 0, 0); + if (type == "oauth2") { + m_credentialFields[type]["consumerKey"] = createLineEdit(credentialsWidget, m_site->settings()->value("auth/consumerKey").toString(), false); + m_credentialFields[type]["consumerSecret"] = createLineEdit(credentialsWidget, m_site->settings()->value("auth/consumerSecret").toString(), false); + formLayout->addRow(tr("Consumer key"), m_credentialFields[type]["consumerKey"]); + formLayout->addRow(tr("Consumer secret"), m_credentialFields[type]["consumerSecret"]); + fields.insert("consumerKey", m_credentialFields[type]["consumerKey"]); + fields.insert("consumerSecret", m_credentialFields[type]["consumerSecret"]); + } else { + auto fieldAuth = dynamic_cast(it.value()); + if (fieldAuth) { + for (AuthField *field : fieldAuth->fields()) { + const QString fid = field->id(); + if (fid.isEmpty()) { + continue; + } + m_credentialFields[type][fid] = createLineEdit(credentialsWidget, field->value(m_site->settings()), field->type() == AuthField::Password); + formLayout->addRow(fieldLabels.contains(fid) ? fieldLabels[fid] : fid, m_credentialFields[type][fid]); + fields.insert(fid, m_credentialFields[type][fid]); + } + } + } + credentialsWidget->setLayout(formLayout); + ui->stackedCredentials->addWidget(credentialsWidget); + } + for (const QString key : fields.keys()) { + const QList l = fields.values(key); + for (int i = 0; i < l.count() - 1; ++i) { + for (int j = i + 1; j < l.count(); ++j) { + connect(l[i], &QLineEdit::textChanged, l[j], &QLineEdit::setText); + connect(l[j], &QLineEdit::textChanged, l[i], &QLineEdit::setText); + } + } + } // Cookies QList cookies = site->cookies(); ui->tableCookies->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); ui->tableCookies->setRowCount(cookies.count()); int cookieRow = 0; - for (const QNetworkCookie &cookie : site->cookies()) - { + for (const QNetworkCookie &cookie : site->cookies()) { ui->tableCookies->setItem(cookieRow, 0, new QTableWidgetItem(QString(cookie.name()))); ui->tableCookies->setItem(cookieRow, 1, new QTableWidgetItem(QString(cookie.value()))); cookieRow++; @@ -114,17 +155,14 @@ SourcesSettingsWindow::SourcesSettingsWindow(Profile *profile, Site *site, QWidg ui->tableHeaders->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); ui->tableHeaders->setRowCount(headers.count()); int headerRow = 0; - for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) - { + for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) { ui->tableHeaders->setItem(headerRow, 0, new QTableWidgetItem(it.key())); ui->tableHeaders->setItem(headerRow, 1, new QTableWidgetItem(it.value().toString())); headerRow++; } // Hide login testing buttons if we can't tests this site's login - if (!m_site->canTestLogin()) - { - ui->widgetTestCredentials->hide(); + if (!m_site->canTestLogin()) { ui->widgetTestLogin->hide(); } } @@ -143,23 +181,10 @@ void SourcesSettingsWindow::addHeader() ui->tableHeaders->setRowCount(ui->tableHeaders->rowCount() + 1); } -void SourcesSettingsWindow::on_buttonAuthHash_clicked() -{ - QString salt = m_site->getApis().first()->value("PasswordSalt"); - QString password = QInputDialog::getText(this, tr("Hash a password"), tr("Please enter your password below.
It will then be hashed using the format \"%1\".").arg(salt)); - if (!password.isEmpty()) - { - salt.replace("%password%", password); - salt.replace("%value%", password); - ui->lineAuthPassword->setText(QCryptographicHash::hash(salt.toUtf8(), QCryptographicHash::Sha1).toHex()); - } -} - void SourcesSettingsWindow::deleteSite() { const int reponse = QMessageBox::question(this, tr("Delete a site"), tr("Are you sure you want to delete the site %1?").arg(m_site->name()), QMessageBox::Yes | QMessageBox::No); - if (reponse == QMessageBox::Yes) - { + if (reponse == QMessageBox::Yes) { QFile f(m_site->getSource()->getPath() + "/sites.txt"); f.open(QIODevice::ReadOnly); QString sites = f.readAll(); @@ -208,7 +233,6 @@ void SourcesSettingsWindow::loginTested(Site *site, Site::LoginResult result) void SourcesSettingsWindow::setLoginStatus(const QString &msg) { const QString italic = QStringLiteral("%1").arg(msg); - ui->labelTestCredentials->setText(italic); ui->labelTestLogin->setText(italic); } @@ -225,7 +249,6 @@ void SourcesSettingsWindow::saveSettings() m_site->setSetting("ignore/1", ui->spinIgnore1->value(), 0); m_site->setSetting("ssl", ui->checkSsl->isChecked(), false); - m_site->setSetting("download/imagesperpage", ui->spinImagesPerPage->value(), 200); m_site->setSetting("download/simultaneous", ui->spinSimultaneousDownloads->value(), 10); m_site->setSetting("download/throttle_details", ui->spinThrottleDetails->value(), 0); m_site->setSetting("download/throttle_image", ui->spinThrottleImage->value(), 0); @@ -235,8 +258,7 @@ void SourcesSettingsWindow::saveSettings() QStringList defs = QStringList() << "xml" << "json" << "regex" << "rss"; QStringList sources = QStringList() << ""; - for (Api *api : m_site->getApis()) - { + for (Api *api : m_site->getSource()->getApis()) { const QString name = api->getName().toLower(); sources.append(name == "html" ? "regex" : name); } @@ -253,46 +275,32 @@ void SourcesSettingsWindow::saveSettings() // Ensure at least one source is selected bool allEmpty = true; - for (const QString &chos : qAsConst(chosen)) - if (!chos.isEmpty()) + for (const QString &chos : qAsConst(chosen)) { + if (!chos.isEmpty()) { allEmpty = false; - if (allEmpty) - { + } + } + if (allEmpty) { QMessageBox::critical(this, tr("Error"), tr("You should at least select one source")); return; } - m_site->setSetting("auth/pseudo", ui->lineAuthPseudo->text(), ""); - m_site->setSetting("auth/password", ui->lineAuthPassword->text(), ""); - // Login - QStringList types = QStringList() << "url" << "get" << "post" << "oauth1" << "oauth2"; - m_site->setSetting("login/type", types[ui->comboLoginType->currentIndex()], "url"); - m_site->setSetting("login/get/url", ui->lineLoginGetUrl->text(), ""); - m_site->setSetting("login/get/pseudo", ui->lineLoginGetPseudo->text(), ""); - m_site->setSetting("login/get/password", ui->lineLoginGetPassword->text(), ""); - m_site->setSetting("login/get/cookie", ui->lineLoginGetCookie->text(), ""); - m_site->setSetting("login/post/url", ui->lineLoginPostUrl->text(), ""); - m_site->setSetting("login/post/pseudo", ui->lineLoginPostPseudo->text(), ""); - m_site->setSetting("login/post/password", ui->lineLoginPostPassword->text(), ""); - m_site->setSetting("login/post/cookie", ui->lineLoginPostCookie->text(), ""); - m_site->setSetting("login/oauth1/requestTokenUrl", ui->lineLoginOAuth1RequestTokenUrl->text(), ""); - m_site->setSetting("login/oauth1/authorizeUrl", ui->lineLoginOAuth1AuthorizeUrl->text(), ""); - m_site->setSetting("login/oauth1/accessTokenUrl", ui->lineLoginOAuth1AccessTokenUrl->text(), ""); - m_site->setSetting("login/oauth2/requestUrl", ui->lineLoginOAuth2RequestUrl->text(), ""); - m_site->setSetting("login/oauth2/tokenUrl", ui->lineLoginOAuth2TokenUrl->text(), ""); - m_site->setSetting("login/oauth2/refreshTokenUrl", ui->lineLoginOAuth2RefreshTokenUrl->text(), ""); - m_site->setSetting("login/oauth2/scope", ui->lineLoginOAuth2Scope->text(), ""); - m_site->setSetting("login/maxPage", ui->spinLoginMaxPage->value(), 0); + m_site->setSetting("login/type", ui->comboLoginType->currentData(), "url"); + for (auto itt = m_credentialFields.begin(); itt != m_credentialFields.end(); ++itt) { + for (auto itf = itt.value().begin(); itf != itt.value().end(); ++itf) { + m_site->setSetting("auth/" + itf.key(), itf.value()->text(), ""); + } + } // Cookies QList cookies; - for (int i = 0; i < ui->tableCookies->rowCount(); ++i) - { + for (int i = 0; i < ui->tableCookies->rowCount(); ++i) { QTableWidgetItem *key = ui->tableCookies->item(i, 0); QTableWidgetItem *value = ui->tableCookies->item(i, 1); - if (key == nullptr || key->text().isEmpty()) + if (key == nullptr || key->text().isEmpty()) { continue; + } QNetworkCookie cookie; cookie.setName(key->text().toLatin1()); @@ -303,12 +311,12 @@ void SourcesSettingsWindow::saveSettings() // Headers QMap headers; - for (int i = 0; i < ui->tableHeaders->rowCount(); ++i) - { + for (int i = 0; i < ui->tableHeaders->rowCount(); ++i) { QTableWidgetItem *key = ui->tableHeaders->item(i, 0); QTableWidgetItem *value = ui->tableHeaders->item(i, 1); - if (key == nullptr || key->text().isEmpty()) + if (key == nullptr || key->text().isEmpty()) { continue; + } headers.insert(key->text(), value != nullptr ? value->text().toLatin1() : ""); } diff --git a/gui/src/sources/sources-settings-window.h b/gui/src/sources/sources-settings-window.h index f3e9f0463..2d2fe1dd6 100644 --- a/gui/src/sources/sources-settings-window.h +++ b/gui/src/sources/sources-settings-window.h @@ -2,6 +2,8 @@ #define SOURCESSETTINGSWINDOW_H #include +#include +#include #include "models/site.h" @@ -12,6 +14,9 @@ namespace Ui class Profile; +class QLineEdit; +class QSettings; +class QWidget; class SourcesSettingsWindow : public QDialog { @@ -22,7 +27,6 @@ class SourcesSettingsWindow : public QDialog ~SourcesSettingsWindow() override; public slots: - void on_buttonAuthHash_clicked(); void deleteSite(); void addCookie(); void addHeader(); @@ -41,6 +45,7 @@ class SourcesSettingsWindow : public QDialog Ui::SourcesSettingsWindow *ui; Site *m_site; QSettings *m_globalSettings; + QMap> m_credentialFields; }; #endif // SOURCESSETTINGSWINDOW_H diff --git a/gui/src/sources/sources-settings-window.ui b/gui/src/sources/sources-settings-window.ui index 5aa6dfdec..c624bd92e 100644 --- a/gui/src/sources/sources-settings-window.ui +++ b/gui/src/sources/sources-settings-window.ui @@ -186,14 +186,14 @@ Download - + Max simultaneous downloads - + 1 @@ -203,63 +203,49 @@ - - + + - Images per page + Interval (thumbnail) - - - - 1 + + + + s - 1000 - - - 200 + 3600 - - - Interval (thumbnail) - - - - Interval (image) - - - - Interval (page) + + + + s - - - - - - Interval (details) + + 3600 - - + + - Interval (error) + Interval (page) - - + + s @@ -268,18 +254,15 @@ - - - - s - - - 3600 + + + + Interval (details) - + s @@ -288,17 +271,14 @@ - - - - s - - - 3600 + + + + Interval (error) - + s @@ -507,369 +487,27 @@
- - - Credentials - - - - - - Username - - - - - - - - - - Password - - - - - - - - - QLineEdit::Password - - - - - - - Hash password - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Test - - - - - - - - Login - + - + Type - - - 0 - - - - Through URL - - - - - GET - - - - - POST - - - - - OAuth 1 - - - - - OAuth 2 - - - + - - - 0 - - - - - - 18 - - - 0 - - - 0 - - - 0 - - - - - URL - - - - - - - - - - Username field - - - - - - - - - - Password field - - - - - - - - - - Cookie - - - - - - - - - - - - 18 - - - 0 - - - 0 - - - 0 - - - - - URL - - - - - - - - - - Username - - - - - - - - - - Password - - - - - - - - - - Cookie - - - - - - - - - - - false - - - - 18 - - - 0 - - - 0 - - - 0 - - - - - Request token url - - - - - - - Authorize url - - - - - - - Access token url - - - - - - - - - - - - - - - - - false - - - - 18 - - - 0 - - - 0 - - - 0 - - - - - Request url - - - - - - - - - - Token url - - - - - - - - - - Refresh token url - - - - - - - - - - Scope - - - - - - - - - + - - - - Page limit - - - - - - - 10000 - - - - + - + 0 @@ -890,7 +528,7 @@ - + Test @@ -1020,16 +658,12 @@ comboRefererPreview comboRefererImage checkSsl - spinImagesPerPage spinSimultaneousDownloads checkSourcesDefault comboSources1 comboSources2 comboSources3 comboSources4 - lineAuthPseudo - lineAuthPassword - buttonAuthHash buttonCancel buttonAccept @@ -1069,22 +703,6 @@ - - pushButton_5 - clicked() - SourcesSettingsWindow - testLogin() - - - 507 - 218 - - - 256 - 193 - - - checkSourcesDefault toggled(bool) @@ -1102,14 +720,14 @@ - buttonTestCredentials + buttonTestLogin clicked() SourcesSettingsWindow testLogin() 507 - 119 + 87 273 @@ -1149,22 +767,6 @@ - - comboLoginType - currentIndexChanged(int) - stackedWidget - setCurrentIndex(int) - - - 146 - 40 - - - 101 - 66 - - - buttonAccept clicked() @@ -1181,6 +783,22 @@ + + comboLoginType + currentIndexChanged(int) + stackedCredentials + setCurrentIndex(int) + + + 188 + 56 + + + 181 + 65 + + + deleteSite() @@ -1188,5 +806,6 @@ addCookie() addHeader() save() + setLoginType(int) diff --git a/gui/src/sources/sources-window.cpp b/gui/src/sources/sources-window.cpp index 6dec6c85e..d3ce53bc5 100644 --- a/gui/src/sources/sources-window.cpp +++ b/gui/src/sources/sources-window.cpp @@ -66,23 +66,25 @@ void SourcesWindow::checkUpdate() { bool oneChecked = false; bool oneUnchecked = false; - for (const auto &row : qAsConst(m_rows)) - { - if (row.check->isChecked()) - { oneChecked = true; } - else - { oneUnchecked = true; } + for (const auto &row : qAsConst(m_rows)) { + if (row.check->isChecked()) { + oneChecked = true; + } else { + oneUnchecked = true; + } + } + if (oneChecked && !oneUnchecked) { + ui->checkBox->setCheckState(Qt::Checked); + } else if (!oneChecked && oneUnchecked) { + ui->checkBox->setCheckState(Qt::Unchecked); + } else { + ui->checkBox->setCheckState(Qt::PartiallyChecked); } - if (oneChecked && !oneUnchecked) - { ui->checkBox->setCheckState(Qt::Checked); } - else if (!oneChecked && oneUnchecked) - { ui->checkBox->setCheckState(Qt::Unchecked); } - else - { ui->checkBox->setCheckState(Qt::PartiallyChecked); } // Update preset save button - if (ui->comboPresets->currentIndex() > 0) - { ui->buttonPresetSave->setEnabled(true); } + if (ui->comboPresets->currentIndex() > 0) { + ui->buttonPresetSave->setEnabled(true); + } } /** @@ -90,8 +92,9 @@ void SourcesWindow::checkUpdate() */ void SourcesWindow::checkClicked() { - if (ui->checkBox->checkState() != Qt::Unchecked) - { ui->checkBox->setCheckState(Qt::Checked); } + if (ui->checkBox->checkState() != Qt::Unchecked) { + ui->checkBox->setCheckState(Qt::Checked); + } checkAll(ui->checkBox->checkState()); } @@ -123,11 +126,11 @@ void SourcesWindow::settingsSite(const QString &site) void SourcesWindow::deleteSite(const QString &site) { int index = -1; - for (int i = 0; i < m_rows.count(); ++i) - { + for (int i = 0; i < m_rows.count(); ++i) { const auto &row = m_rows[i]; - if (row.site->url() != site) + if (row.site->url() != site) { continue; + } row.check->hide(); ui->gridLayout->removeWidget(row.check); @@ -137,8 +140,7 @@ void SourcesWindow::deleteSite(const QString &site) ui->gridLayout->removeWidget(row.button); row.button->deleteLater(); - for (QLabel *label : qAsConst(row.labels)) - { + for (QLabel *label : qAsConst(row.labels)) { label->hide(); ui->gridLayout->removeWidget(label); label->deleteLater(); @@ -150,8 +152,7 @@ void SourcesWindow::deleteSite(const QString &site) index = i; } - if (index != -1) - { + if (index != -1) { m_rows.removeAt(index); m_siteRows.remove(site); } @@ -175,16 +176,14 @@ void SourcesWindow::updateCheckboxes() void SourcesWindow::removeCheckboxes() { - for (auto &row : qAsConst(m_rows)) - { + for (auto &row : qAsConst(m_rows)) { ui->gridLayout->removeWidget(row.check); row.check->deleteLater(); ui->gridLayout->removeWidget(row.button); row.button->deleteLater(); - for (QLabel *label : qAsConst(row.labels)) - { + for (QLabel *label : qAsConst(row.labels)) { ui->gridLayout->removeWidget(label); label->deleteLater(); } @@ -201,8 +200,7 @@ void SourcesWindow::addCheckboxes() QString t = m_profile->getSettings()->value("Sources/Types", "icon").toString(); int i = 0; - for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) - { + for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) { Site *site = it.value(); SourceRow row; @@ -216,10 +214,8 @@ void SourcesWindow::addCheckboxes() ui->gridLayout->addWidget(check, i, 0); int n = 1; - if (t != "hide") - { - if (t == "icon" || t == "both") - { + if (t != "hide") { + if (t == "icon" || t == "both") { QAffiche *image = new QAffiche(it.key(), 0, QColor(), this); image->setPixmap(QPixmap(site->getSource()->getPath() + "/icon.png").scaled(QSize(16, 16))); image->setCursor(Qt::PointingHandCursor); @@ -228,8 +224,7 @@ void SourcesWindow::addCheckboxes() row.labels.append(image); n++; } - if (t == "text" || t == "both") - { + if (t == "text" || t == "both") { QLabel *type = new QLabel(site->getSource()->getName(), this); ui->gridLayout->addWidget(type, i, n); row.labels.append(type); @@ -251,14 +246,11 @@ void SourcesWindow::addCheckboxes() } /*int n = 0+(t == "icon" || t == "both")+(t == "text" || t == "both"); - for (int i = 0; i < m_checks.count(); i++) - { + for (int i = 0; i < m_checks.count(); i++) { ui->gridLayout->addWidget(m_checks.at(i), i, 0); m_checks.at(i)->show(); - if (!m_labels.isEmpty()) - { - for (int r = 0; r < n; r++) - { + if (!m_labels.isEmpty()) { + for (int r = 0; r < n; r++) { ui->gridLayout->addWidget(m_labels.at(i*n+r), i*n+r, 1); m_labels.at(i*n+r)->show(); } @@ -275,14 +267,14 @@ void SourcesWindow::addCheckboxes() */ void SourcesWindow::checkAll(int check) { - for (const auto &row : qAsConst(m_rows)) + for (const auto &row : qAsConst(m_rows)) { row.check->setChecked(check == 2); + } } void SourcesWindow::checkForUpdates() { - for (auto it = m_sources.constBegin(); it != m_sources.constEnd(); ++it) - { + for (auto it = m_sources.constBegin(); it != m_sources.constEnd(); ++it) { const SourceUpdater &updater = it.value()->getUpdater(); connect(&updater, &SourceUpdater::finished, this, &SourcesWindow::checkForUpdatesReceived); updater.checkForUpdates(); @@ -290,14 +282,15 @@ void SourcesWindow::checkForUpdates() } void SourcesWindow::checkForUpdatesReceived(const QString &sourceName, bool isNew) { - if (!isNew) + if (!isNew) { return; + } Source *source = m_sources[sourceName]; - for (Site *site : source->getSites()) - { - if (!m_siteRows.contains(site->url())) + for (Site *site : source->getSites()) { + if (!m_siteRows.contains(site->url())) { continue; + } int pos = m_siteRows.value(site->url()); m_rows[pos].labels[0]->setPixmap(QPixmap(":/images/icons/update.png")); @@ -314,23 +307,25 @@ void SourcesWindow::checkForSourceIssues() } void SourcesWindow::checkForSourceIssuesReceived() { - if (m_checkForSourceReply->error() != QNetworkReply::NoError) + if (m_checkForSourceReply->error() != QNetworkReply::NoError) { return; + } QString source = m_checkForSourceReply->readAll(); QStringList issues = source.split("\n"); - for (const QString &issue : issues) - { + for (const QString &issue : issues) { const int index = issue.indexOf(':'); - if (issue.isEmpty() || index < 0) + if (issue.isEmpty() || index < 0) { return; + } const QString &site = issue.left(index).trimmed(); const QString &desc = issue.mid(index + 1).trimmed(); - if (!m_siteRows.contains(site)) + if (!m_siteRows.contains(site)) { continue; + } int pos = m_siteRows.value(site); m_rows[pos].labels[0]->setPixmap(QPixmap(":/images/icons/warning.png")); @@ -345,8 +340,7 @@ QMap SourcesWindow::loadPresets(QSettings *settings) const QMap ret; const int size = settings->beginReadArray("SourcePresets"); - for (int i = 0; i < size; ++i) - { + for (int i = 0; i < size; ++i) { settings->setArrayIndex(i); const QString name = settings->value("name").toString(); const QStringList sources = settings->value("sources").toStringList(); @@ -361,8 +355,7 @@ void SourcesWindow::savePresets(QSettings *settings) const { settings->beginWriteArray("SourcePresets"); int i = 0; - for (auto it = m_presets.constBegin(); it != m_presets.constEnd(); ++it) - { + for (auto it = m_presets.constBegin(); it != m_presets.constEnd(); ++it) { settings->setArrayIndex(i); settings->setValue("name", it.key()); settings->setValue("sources", it.value()); @@ -374,9 +367,11 @@ void SourcesWindow::savePresets(QSettings *settings) const QList SourcesWindow::selected() const { QList selected; - for (const auto &row : qAsConst(m_rows)) - if (row.check->isChecked()) + for (const auto &row : qAsConst(m_rows)) { + if (row.check->isChecked()) { selected.append(row.site); + } + } return selected; } @@ -397,14 +392,16 @@ void SourcesWindow::addPreset() { bool ok; QString name = QInputDialog::getText(this, tr("Create a new preset"), tr("Name"), QLineEdit::Normal, QString(), &ok); - if (!ok || name.isEmpty()) + if (!ok || name.isEmpty()) { return; + } const QList &selectedSites = selected(); QStringList sel; sel.reserve(selectedSites.count()); - for (Site *site : selectedSites) + for (Site *site : selectedSites) { sel.append(site->url()); + } m_presets.insert(name, sel); showPresets(); @@ -422,8 +419,9 @@ void SourcesWindow::editPreset() bool ok; QString oldName = ui->comboPresets->currentText(); QString newName = QInputDialog::getText(this, tr("Edit preset"), tr("Name"), QLineEdit::Normal, oldName, &ok); - if (!ok || newName.isEmpty()) + if (!ok || newName.isEmpty()) { return; + } m_presets.insert(newName, m_presets[oldName]); m_presets.remove(oldName); @@ -438,8 +436,9 @@ void SourcesWindow::savePreset() QStringList sel; sel.reserve(selectedSites.count()); - for (Site *site : selectedSites) + for (Site *site : selectedSites) { sel.append(site->url()); + } m_presets[ui->comboPresets->currentText()] = sel; ui->buttonPresetSave->setEnabled(false); @@ -450,11 +449,11 @@ void SourcesWindow::selectPreset(const QString &name) { bool isPreset = ui->comboPresets->currentIndex() > 0; - if (isPreset) - { + if (isPreset) { const QStringList &preset = m_presets[name]; - for (const auto &row : qAsConst(m_rows)) + for (const auto &row : qAsConst(m_rows)) { row.check->setChecked(preset.contains(row.site->url())); + } } ui->buttonPresetSave->setEnabled(false); diff --git a/gui/src/tabs/downloads-tab.cpp b/gui/src/tabs/downloads-tab.cpp index 637661a70..98f9efadd 100644 --- a/gui/src/tabs/downloads-tab.cpp +++ b/gui/src/tabs/downloads-tab.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) #include #endif @@ -19,10 +20,10 @@ #include "downloader/download-query-group.h" #include "downloader/download-query-image.h" #include "downloader/download-query-loader.h" -#include "downloader/downloader.h" #include "downloader/image-downloader.h" #include "functions.h" #include "helpers.h" +#include "loader/pack-loader.h" #include "logger.h" #include "main-window.h" #include "models/filename.h" @@ -43,8 +44,9 @@ DownloadsTab::DownloadsTab(Profile *profile, MainWindow *parent) QStringList sizes = m_settings->value("batch", "100,100,100,100,100,100,100,100,100").toString().split(','); int m = sizes.size() > ui->tableBatchGroups->columnCount() ? ui->tableBatchGroups->columnCount() : sizes.size(); - for (int i = 0; i < m; i++) - { ui->tableBatchGroups->horizontalHeader()->resizeSection(i, sizes.at(i).toInt()); } + for (int i = 0; i < m; i++) { + ui->tableBatchGroups->horizontalHeader()->resizeSection(i, sizes.at(i).toInt()); + } QShortcut *actionDeleteBatchGroups = new QShortcut(QKeySequence::Delete, ui->tableBatchGroups); actionDeleteBatchGroups->setContext(Qt::WidgetWithChildrenShortcut); @@ -66,8 +68,7 @@ DownloadsTab::~DownloadsTab() void DownloadsTab::changeEvent(QEvent *event) { // Automatically re-translate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } @@ -80,8 +81,9 @@ void DownloadsTab::closeEvent(QCloseEvent *event) QStringList sizes; sizes.reserve(ui->tableBatchGroups->columnCount()); - for (int i = 0; i < ui->tableBatchGroups->columnCount(); i++) - { sizes.append(QString::number(ui->tableBatchGroups->horizontalHeader()->sectionSize(i))); } + for (int i = 0; i < ui->tableBatchGroups->columnCount(); i++) { + sizes.append(QString::number(ui->tableBatchGroups->horizontalHeader()->sectionSize(i))); + } m_settings->setValue("batch", sizes.join(",")); } @@ -89,20 +91,20 @@ void DownloadsTab::closeEvent(QCloseEvent *event) void DownloadsTab::siteDeleted(Site *site) { QList batchRows; - for (int i = 0; i < m_groupBatchs.count(); ++i) - { + for (int i = 0; i < m_groupBatchs.count(); ++i) { const DownloadQueryGroup &batch = m_groupBatchs[i]; - if (batch.site == site) + if (batch.site == site) { batchRows.append(i); + } } batchRemoveGroups(batchRows); QList uniquesRows; - for (int i = 0; i < m_batchs.count(); ++i) - { + for (int i = 0; i < m_batchs.count(); ++i) { const DownloadQueryImage &batch = m_batchs[i]; - if (batch.site == site) + if (batch.site == site) { uniquesRows.append(i); + } } batchRemoveUniques(uniquesRows); } @@ -110,13 +112,15 @@ void DownloadsTab::siteDeleted(Site *site) void DownloadsTab::batchClear() { // Don't do anything if there's nothing to clear - if (ui->tableBatchGroups->rowCount() == 0 && ui->tableBatchUniques->rowCount() == 0) + if (ui->tableBatchGroups->rowCount() == 0 && ui->tableBatchUniques->rowCount() == 0) { return; + } // Confirm deletion auto reponse = QMessageBox::question(this, tr("Confirmation"), tr("Are you sure you want to clear your download list?"), QMessageBox::Yes | QMessageBox::No); - if (reponse != QMessageBox::Yes) + if (reponse != QMessageBox::Yes) { return; + } m_batchs.clear(); ui->tableBatchUniques->clearContents(); @@ -136,11 +140,11 @@ void DownloadsTab::batchClearSel() void DownloadsTab::batchClearSelGroups() { QList rows; - for (QTableWidgetItem *selected : ui->tableBatchGroups->selectedItems()) - { + for (QTableWidgetItem *selected : ui->tableBatchGroups->selectedItems()) { int row = selected->row(); - if (!rows.contains(row)) + if (!rows.contains(row)) { rows.append(row); + } } batchRemoveGroups(rows); @@ -148,11 +152,11 @@ void DownloadsTab::batchClearSelGroups() void DownloadsTab::batchClearSelUniques() { QList rows; - for (QTableWidgetItem *selected : ui->tableBatchUniques->selectedItems()) - { + for (QTableWidgetItem *selected : ui->tableBatchUniques->selectedItems()) { int row = selected->row(); - if (!rows.contains(row)) + if (!rows.contains(row)) { rows.append(row); + } } batchRemoveUniques(rows); @@ -162,8 +166,7 @@ void DownloadsTab::batchRemoveGroups(QList rows) std::sort(rows.begin(), rows.end()); int rem = 0; - for (int i : qAsConst(rows)) - { + for (int i : qAsConst(rows)) { int pos = i - rem; m_progressBars[pos]->deleteLater(); m_progressBars.removeAt(pos); @@ -179,8 +182,7 @@ void DownloadsTab::batchRemoveUniques(QList rows) std::sort(rows.begin(), rows.end()); int rem = 0; - for (int i : qAsConst(rows)) - { + for (int i : qAsConst(rows)) { int pos = i - rem; ui->tableBatchUniques->removeRow(pos); m_batchs.removeAt(pos); @@ -193,21 +195,22 @@ void DownloadsTab::batchRemoveUniques(QList rows) void DownloadsTab::batchMove(int diff) { QList selected = ui->tableBatchGroups->selectedItems(); - if (selected.isEmpty()) + if (selected.isEmpty()) { return; + } QSet rows; - for (QTableWidgetItem *item : selected) + for (QTableWidgetItem *item : selected) { rows.insert(item->row()); + } - for (int sourceRow : rows) - { + for (int sourceRow : rows) { int destRow = sourceRow + diff; - if (destRow < 0 || destRow >= ui->tableBatchGroups->rowCount()) + if (destRow < 0 || destRow >= ui->tableBatchGroups->rowCount()) { return; + } - for (int col = 0; col < ui->tableBatchGroups->columnCount(); ++col) - { + for (int col = 0; col < ui->tableBatchGroups->columnCount(); ++col) { QTableWidgetItem *sourceItem = ui->tableBatchGroups->takeItem(sourceRow, col); QTableWidgetItem *destItem = ui->tableBatchGroups->takeItem(destRow, col); @@ -217,8 +220,7 @@ void DownloadsTab::batchMove(int diff) } QItemSelection selection; - for (int i = 0; i < selected.count(); i++) - { + for (int i = 0; i < selected.count(); i++) { QModelIndex index = ui->tableBatchGroups->model()->index(selected.at(i)->row(), selected.at(i)->column()); selection.select(index, index); } @@ -238,48 +240,51 @@ void DownloadsTab::batchMoveDown() void DownloadsTab::updateBatchGroups(int y, int x) { - if (m_allow && x > 0) - { + if (m_allow && x > 0) { QString val = ui->tableBatchGroups->item(y, x)->text(); int toInt = val.toInt(); + m_groupBatchs[y].progressVal = 0; + m_groupBatchs[y].progressFinished = false; + switch (x) { - case 1: m_groupBatchs[y].tags = val; break; + case 1: + if (m_groupBatchs[y].query.gallery.isNull()) { + m_groupBatchs[y].query.tags = val.split(' ', QString::SkipEmptyParts); + } + break; + case 3: m_groupBatchs[y].page = toInt; break; case 6: m_groupBatchs[y].filename = val; break; case 7: m_groupBatchs[y].path = val; break; case 8: m_groupBatchs[y].postFiltering = val.split(' ', QString::SkipEmptyParts); break; case 9: m_groupBatchs[y].getBlacklisted = (val != "false"); break; + case 10: m_groupBatchs[y].galleriesCountAsOne = (val != "false"); break; case 2: - if (!m_profile->getSites().contains(val)) - { + if (!m_profile->getSites().contains(val)) { error(this, tr("This source is not valid.")); ui->tableBatchGroups->item(y, x)->setText(m_groupBatchs[y].site->url()); + } else { + m_groupBatchs[y].site = m_profile->getSites().value(val); } - else - { m_groupBatchs[y].site = m_profile->getSites().value(val); } break; case 4: - if (toInt < 1) - { + if (toInt < 1) { error(this, tr("The image per page value must be greater or equal to 1.")); ui->tableBatchGroups->item(y, x)->setText(QString::number(m_groupBatchs[y].perpage)); + } else { + m_groupBatchs[y].perpage = toInt; } - else - { m_groupBatchs[y].perpage = toInt; } break; case 5: - if (toInt < 0) - { + if (toInt < 0) { error(this, tr("The image limit must be greater or equal to 0.")); ui->tableBatchGroups->item(y, x)->setText(QString::number(m_groupBatchs[y].total)); - } - else - { + } else { m_groupBatchs[y].total = toInt; m_progressBars[y]->setMaximum(toInt); } @@ -307,8 +312,9 @@ void DownloadsTab::addUnique() void DownloadsTab::batchAddGroup(const DownloadQueryGroup &values) { // Ignore downloads already present in the list - if (m_groupBatchs.contains(values)) + if (m_groupBatchs.contains(values)) { return; + } m_groupBatchs.append(values); int pos = m_groupBatchs.count(); @@ -321,7 +327,11 @@ void DownloadsTab::batchAddGroup(const DownloadQueryGroup &values) item->setFlags(item->flags() ^ Qt::ItemIsEditable); ui->tableBatchGroups->setItem(row, 0, item); - addTableItem(ui->tableBatchGroups, row, 1, values.tags); + auto *gItem = addTableItem(ui->tableBatchGroups, row, 1, values.query.toString()); + if (!values.query.gallery.isNull()) { + gItem->setFlags(gItem->flags() & (~Qt::ItemIsEditable)); + } + addTableItem(ui->tableBatchGroups, row, 2, values.site->url()); addTableItem(ui->tableBatchGroups, row, 3, QString::number(values.page)); addTableItem(ui->tableBatchGroups, row, 4, QString::number(values.perpage)); @@ -330,12 +340,13 @@ void DownloadsTab::batchAddGroup(const DownloadQueryGroup &values) addTableItem(ui->tableBatchGroups, row, 7, values.path); addTableItem(ui->tableBatchGroups, row, 8, values.postFiltering.join(' ')); addTableItem(ui->tableBatchGroups, row, 9, values.getBlacklisted ? "true" : "false"); + addTableItem(ui->tableBatchGroups, row, 10, values.galleriesCountAsOne ? "true" : "false"); auto *prog = new QProgressBar(this); prog->setTextVisible(false); prog->setMaximum(values.total); m_progressBars.append(prog); - ui->tableBatchGroups->setCellWidget(row, 10, prog); + ui->tableBatchGroups->setCellWidget(row, 11, prog); m_allow = true; saveLinkList(m_profile->getPath() + "/restore.igl"); @@ -344,43 +355,47 @@ void DownloadsTab::batchAddGroup(const DownloadQueryGroup &values) void DownloadsTab::updateGroupCount() { int groups = 0; - for (int i = 0; i < ui->tableBatchGroups->rowCount(); i++) + for (int i = 0; i < ui->tableBatchGroups->rowCount(); i++) { groups += ui->tableBatchGroups->item(i, 5)->text().toInt(); + } ui->labelGroups->setText(tr("Groups (%1/%2)").arg(ui->tableBatchGroups->rowCount()).arg(groups)); } void DownloadsTab::batchAddUnique(const DownloadQueryImage &query, bool save) { // Ignore downloads already present in the list - if (m_batchs.contains(query)) + if (m_batchs.contains(query)) { return; + } - log(QStringLiteral("Adding single image: %1").arg(query.values["file_url"]), Logger::Info); + log(QStringLiteral("Adding single image: %1").arg(query.image->fileUrl().toString()), Logger::Info); m_batchs.append(query); ui->tableBatchUniques->setRowCount(ui->tableBatchUniques->rowCount() + 1); int row = ui->tableBatchUniques->rowCount() - 1; - addTableItem(ui->tableBatchUniques, row, 0, query.values["id"]); - addTableItem(ui->tableBatchUniques, row, 1, query.values["md5"]); - addTableItem(ui->tableBatchUniques, row, 2, query.values["rating"]); - addTableItem(ui->tableBatchUniques, row, 3, query.values["tags"]); - addTableItem(ui->tableBatchUniques, row, 4, query.values["file_url"]); - addTableItem(ui->tableBatchUniques, row, 5, query.values["date"]); - addTableItem(ui->tableBatchUniques, row, 6, query.values["search"]); + addTableItem(ui->tableBatchUniques, row, 0, QString::number(query.image->id())); + addTableItem(ui->tableBatchUniques, row, 1, query.image->md5()); + addTableItem(ui->tableBatchUniques, row, 2, query.image->rating()); + addTableItem(ui->tableBatchUniques, row, 3, query.image->tagsString().join(' ')); + addTableItem(ui->tableBatchUniques, row, 4, query.image->fileUrl().toString()); + addTableItem(ui->tableBatchUniques, row, 5, query.image->createdAt().toString(Qt::ISODate)); + addTableItem(ui->tableBatchUniques, row, 6, query.image->search().join(' ')); addTableItem(ui->tableBatchUniques, row, 7, query.site->name()); addTableItem(ui->tableBatchUniques, row, 8, query.filename); addTableItem(ui->tableBatchUniques, row, 9, query.path); - if (save) - { saveLinkList(m_profile->getPath() + "/restore.igl"); } + if (save) { + saveLinkList(m_profile->getPath() + "/restore.igl"); + } } -void DownloadsTab::addTableItem(QTableWidget *table, int row, int col, const QString &text) +QTableWidgetItem *DownloadsTab::addTableItem(QTableWidget *table, int row, int col, const QString &text) { auto *item = new QTableWidgetItem(text); item->setToolTip(text); table->setItem(row, col, item); + return item; } @@ -388,16 +403,18 @@ void DownloadsTab::on_buttonSaveLinkList_clicked() { QString lastDir = m_settings->value("linksLastDir", "").toString(); QString save = QFileDialog::getSaveFileName(this, tr("Save link list"), QDir::toNativeSeparators(lastDir), tr("Imageboard-Grabber links (*.igl)")); - if (save.isEmpty()) - { return; } + if (save.isEmpty()) { + return; + } save = QDir::toNativeSeparators(save); m_settings->setValue("linksLastDir", save.section(QDir::separator(), 0, -2)); - if (saveLinkList(save)) - { QMessageBox::information(this, tr("Save link list"), tr("Link list saved successfully!")); } - else - { QMessageBox::critical(this, tr("Save link list"), tr("Error opening file.")); } + if (saveLinkList(save)) { + QMessageBox::information(this, tr("Save link list"), tr("Link list saved successfully!")); + } else { + QMessageBox::critical(this, tr("Save link list"), tr("Error opening file.")); + } } bool DownloadsTab::saveLinkList(const QString &filename) { @@ -407,40 +424,44 @@ bool DownloadsTab::saveLinkList(const QString &filename) void DownloadsTab::on_buttonLoadLinkList_clicked() { QString load = QFileDialog::getOpenFileName(this, tr("Load link list"), QString(), tr("Imageboard-Grabber links (*.igl)")); - if (load.isEmpty()) - { return; } + if (load.isEmpty()) { + return; + } - if (loadLinkList(load)) - { QMessageBox::information(this, tr("Load link list"), tr("Link list loaded successfully!")); } - else - { QMessageBox::critical(this, tr("Load link list"), tr("Error opening file.")); } + if (loadLinkList(load)) { + QMessageBox::information(this, tr("Load link list"), tr("Link list loaded successfully!")); + } else { + QMessageBox::critical(this, tr("Load link list"), tr("Error opening file.")); + } } bool DownloadsTab::loadLinkList(const QString &filename) { QList newBatchs; QList newGroupBatchs; - if (!DownloadQueryLoader::load(filename, newBatchs, newGroupBatchs, m_profile->getSites())) + if (!DownloadQueryLoader::load(filename, newBatchs, newGroupBatchs, m_profile->getSites())) { return false; + } log(tr("Loading %n download(s)", "", newBatchs.count() + newGroupBatchs.count()), Logger::Info); m_allow = false; - for (const auto &queryImage : qAsConst(newBatchs)) - { + for (const auto &queryImage : qAsConst(newBatchs)) { batchAddUnique(queryImage, false); } - for (const auto &queryGroup : qAsConst(newGroupBatchs)) - { + for (const auto &queryGroup : qAsConst(newGroupBatchs)) { ui->tableBatchGroups->setRowCount(ui->tableBatchGroups->rowCount() + 1); - const QString unk = queryGroup.unk; - const int sep = unk.indexOf("/"); - const int val = unk.leftRef(sep).toInt(); - const int max = unk.midRef(sep + 1).toInt(); + const int val = queryGroup.progressVal; + const int max = queryGroup.total; int row = ui->tableBatchGroups->rowCount() - 1; - addTableItem(ui->tableBatchGroups, row, 1, queryGroup.tags); + + auto *gItem = addTableItem(ui->tableBatchGroups, row, 1, queryGroup.query.toString()); + if (!queryGroup.query.gallery.isNull()) { + gItem->setFlags(gItem->flags() & (~Qt::ItemIsEditable)); + } + addTableItem(ui->tableBatchGroups, row, 2, queryGroup.site->url()); addTableItem(ui->tableBatchGroups, row, 3, QString::number(queryGroup.page)); addTableItem(ui->tableBatchGroups, row, 4, QString::number(queryGroup.perpage)); @@ -449,6 +470,7 @@ bool DownloadsTab::loadLinkList(const QString &filename) addTableItem(ui->tableBatchGroups, row, 7, queryGroup.path); addTableItem(ui->tableBatchGroups, row, 8, queryGroup.postFiltering.join(' ')); addTableItem(ui->tableBatchGroups, row, 9, queryGroup.getBlacklisted ? "true" : "false"); + addTableItem(ui->tableBatchGroups, row, 10, queryGroup.galleriesCountAsOne ? "true" : "false"); m_groupBatchs.append(queryGroup); QTableWidgetItem *it = new QTableWidgetItem(getIcon(":/images/status/" + QString(val >= max ? "ok" : (val > 0 ? "downloading" : "pending")) + ".png"), ""); @@ -462,7 +484,7 @@ bool DownloadsTab::loadLinkList(const QString &filename) prog->setMinimum(0); prog->setTextVisible(false); m_progressBars.append(prog); - ui->tableBatchGroups->setCellWidget(row, 10, prog); + ui->tableBatchGroups->setCellWidget(row, 11, prog); } m_allow = true; updateGroupCount(); @@ -472,8 +494,9 @@ bool DownloadsTab::loadLinkList(const QString &filename) QIcon &DownloadsTab::getIcon(const QString &path) { - if (!m_icons.contains(path)) + if (!m_icons.contains(path)) { m_icons.insert(path, QIcon(path)); + } return m_icons[path]; } @@ -492,25 +515,21 @@ void DownloadsTab::batchSel() void DownloadsTab::getAll(bool all) { // Initial checks - if (m_getAll) - { + if (m_getAll) { log(QStringLiteral("Batch download start cancelled because another one is already running."), Logger::Warning); return; } - if (m_settings->value("Save/path").toString().isEmpty()) - { + if (m_settings->value("Save/path").toString().isEmpty()) { error(this, tr("You did not specify a save folder!")); return; } - if (m_settings->value("Save/filename").toString().isEmpty()) - { + if (m_settings->value("Save/filename").toString().isEmpty()) { error(this, tr("You did not specify a filename!")); return; } log(QStringLiteral("Batch download started."), Logger::Info); - if (m_progressDialog == nullptr) - { + if (m_progressDialog == nullptr) { m_progressDialog = new BatchWindow(m_profile->getSettings(), this); connect(m_progressDialog, &BatchWindow::paused, this, &DownloadsTab::getAllPause); connect(m_progressDialog, &BatchWindow::rejected, this, &DownloadsTab::getAllCancel); @@ -527,56 +546,43 @@ void DownloadsTab::getAll(bool all) m_getAll404s = 0; m_getAllErrors = 0; m_getAllSkipped = 0; - m_downloaders.clear(); + m_getAllResumed = 0; m_getAllRemaining.clear(); m_getAllFailed.clear(); m_getAllDownloading.clear(); m_getAllSkippedImages.clear(); m_batchPending.clear(); - m_lastDownloader = nullptr; - m_waitingDownloaders.clear(); + m_waitingPackLoaders.clear(); + m_currentPackLoader = nullptr; m_batchUniqueDownloading.clear(); - if (!all) - { + if (!all) { QList tdl; - for (QTableWidgetItem *item : ui->tableBatchUniques->selectedItems()) - { + for (QTableWidgetItem *item : ui->tableBatchUniques->selectedItems()) { int row = item->row(); - if (tdl.contains(row)) + if (tdl.contains(row)) { continue; + } tdl.append(row); DownloadQueryImage batch = m_batchs[row]; - Page *page = new Page(m_profile, batch.site, m_profile->getSites().values(), batch.values["search"].split(" "), 1, 1, QStringList(), false, this); - BatchDownloadImage d; - d.image = QSharedPointer(new Image(batch.site, batch.values, m_profile, page)); + d.image = batch.image; d.queryImage = &batch; m_getAllRemaining.append(d); m_batchUniqueDownloading.insert(row); } - } - else - { - for (int j = 0; j < m_batchs.count(); ++j) - { + } else { + for (int j = 0; j < m_batchs.count(); ++j) { const DownloadQueryImage &batch = m_batchs[j]; - if (batch.values.value("file_url").isEmpty()) - { + if (batch.image->fileUrl().isEmpty()) { log(QStringLiteral("No file URL provided in image download query"), Logger::Warning); continue; } - QMap dta = batch.values; - dta.insert("filename", batch.filename); - dta.insert("folder", batch.path); - - Page *page = new Page(m_profile, batch.site, m_profile->getSites().values(), batch.values["search"].split(" "), 1, 1, QStringList(), false, this); - BatchDownloadImage d; - d.image = QSharedPointer(new Image(batch.site, dta, m_profile, page)); + d.image = batch.image; d.queryImage = &batch; m_getAllRemaining.append(d); @@ -586,25 +592,25 @@ void DownloadsTab::getAll(bool all) m_getAllLimit = m_batchs.size(); m_allow = false; - for (int i = 0; i < ui->tableBatchGroups->rowCount(); i++) - { ui->tableBatchGroups->item(i, 0)->setIcon(getIcon(":/images/status/pending.png")); } + for (int i = 0; i < ui->tableBatchGroups->rowCount(); i++) { + ui->tableBatchGroups->item(i, 0)->setIcon(getIcon(":/images/status/pending.png")); + } m_allow = true; m_profile->getCommands().before(); m_batchDownloading.clear(); QSet todownload = QSet(); - for (QTableWidgetItem *item : ui->tableBatchGroups->selectedItems()) - if (!todownload.contains(item->row())) + for (QTableWidgetItem *item : ui->tableBatchGroups->selectedItems()) { + if (!todownload.contains(item->row())) { todownload.insert(item->row()); + } + } - if (all || !todownload.isEmpty()) - { - for (int j = 0; j < m_groupBatchs.count(); ++j) - { - if (all || todownload.contains(j)) - { - if (m_progressBars.length() > j && m_progressBars[j] != nullptr) - { + int resumeCount = 0; + if (all || !todownload.isEmpty()) { + for (int j = 0; j < m_groupBatchs.count(); ++j) { + if (all || todownload.contains(j)) { + if (m_progressBars.length() > j && m_progressBars[j] != nullptr) { m_progressBars[j]->setValue(0); m_progressBars[j]->setMinimum(0); // m_progressBars[j]->setMaximum(100); @@ -614,42 +620,64 @@ void DownloadsTab::getAll(bool all) m_batchPending.insert(j, b); m_getAllLimit += b.total; m_batchDownloading.insert(j); + + if (b.progressVal > 0 && !b.progressFinished) { + resumeCount += b.progressVal; + } } } } - + // Try to resume downloads that were stopped in the middle + bool clear = false; + bool resume = resumeCount > 0; + if (resume) { + int resumeAnswer = QMessageBox::question(this, "", "Some downloads were started but not finished. Do you want to continue from where you left off?"); + if (resumeAnswer == QMessageBox::Yes) { + m_getAllResumed = resumeCount; + } else { + resume = false; + } + } + for (const int b : m_batchDownloading) { + if (m_groupBatchs[b].progressFinished || !resume) { + m_groupBatchs[b].progressVal = 0; + m_batchPending[b].progressVal = 0; + } + m_groupBatchs[b].progressFinished = false; + m_batchPending[b].progressFinished = false; + } // Confirm before downloading possibly more than 10,000 images bool tooBig = false; - if (m_getAllLimit > 10000 && m_settings->value("confirm_big_downloads", true).toBool()) - { + if (m_getAllLimit > 10000 && m_settings->value("confirm_big_downloads", true).toBool()) { QMessageBox msgBox(this); msgBox.setText(tr("You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed?").arg(m_getAllLimit)); msgBox.setIcon(QMessageBox::Warning); QCheckBox dontAskCheckBox(tr("Don't ask me again")); dontAskCheckBox.setCheckable(true); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) - msgBox.setCheckBox(&dontAskCheckBox); -#else - msgBox.addButton(&dontShowCheckBox, QMessageBox::ResetRole); -#endif + #if (QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)) + msgBox.setCheckBox(&dontAskCheckBox); + #else + msgBox.addButton(&dontShowCheckBox, QMessageBox::ResetRole); + #endif msgBox.addButton(QMessageBox::Yes); msgBox.addButton(QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); int response = msgBox.exec(); // Don't close on "cancel" - if (response != QMessageBox::Yes) - { tooBig = true; } + if (response != QMessageBox::Yes) { + tooBig = true; + } // Remember checkbox - else if (dontAskCheckBox.checkState() == Qt::Checked) - { m_settings->setValue("confirm_big_downloads", false); } + else if (dontAskCheckBox.checkState() == Qt::Checked) { + m_settings->setValue("confirm_big_downloads", false); + } } - if (tooBig || (m_batchPending.isEmpty() && m_getAllRemaining.isEmpty())) - { + if (tooBig || (m_batchPending.isEmpty() && m_getAllRemaining.isEmpty())) { log(tooBig ? QStringLiteral("Batch download too big") : QStringLiteral("Nothing to download"), Logger::Info); m_getAll = false; ui->widgetDownloadButtons->setEnabled(true); @@ -666,15 +694,14 @@ void DownloadsTab::getAllLogin() m_progressDialog->setText(tr("Logging in, please wait...")); m_getAllLogins.clear(); - for (auto it = m_batchPending.constBegin(); it != m_batchPending.constEnd(); ++it) - { + for (auto it = m_batchPending.constBegin(); it != m_batchPending.constEnd(); ++it) { Site *site = it.value().site; - if (!m_getAllLogins.contains(site)) - { m_getAllLogins.append(site); } + if (!m_getAllLogins.contains(site)) { + m_getAllLogins.append(site); + } } - if (m_getAllLogins.empty()) - { + if (m_getAllLogins.empty()) { getAllFinishedLogins(); return; } @@ -682,8 +709,7 @@ void DownloadsTab::getAllLogin() m_progressDialog->setCurrentValue(0); m_progressDialog->setCurrentMax(m_getAllLogins.count()); - for (Site *site : m_getAllLogins) - { + for (Site *site : m_getAllLogins) { connect(site, &Site::loggedIn, this, &DownloadsTab::getAllFinishedLogin, Qt::QueuedConnection); site->login(); } @@ -692,65 +718,31 @@ void DownloadsTab::getAllFinishedLogin(Site *site, Site::LoginResult result) { Q_UNUSED(result); - if (m_getAllLogins.empty()) - { return; } + if (m_getAllLogins.empty()) { + return; + } m_progressDialog->setCurrentValue(m_progressDialog->currentValue() + 1); m_getAllLogins.removeAll(site); - if (m_getAllLogins.empty()) - { getAllFinishedLogins(); } + if (m_getAllLogins.empty()) { + getAllFinishedLogins(); + } } void DownloadsTab::getAllFinishedLogins() { bool usePacking = m_settings->value("packing_enable", true).toBool(); - int realConstImagesPerPack = m_settings->value("packing_size", 1000).toInt(); + int imagesPerPack = m_settings->value("packing_size", 1000).toInt(); int total = 0; - for (auto j = m_batchPending.constBegin(); j != m_batchPending.constEnd(); ++j) - { - DownloadQueryGroup b = j.value(); - - int constImagesPerPack = usePacking ? realConstImagesPerPack : b.total; - int pagesPerPack = qCeil(static_cast(constImagesPerPack) / b.perpage); - int imagesPerPack = pagesPerPack * b.perpage; - int packs = qCeil(static_cast(b.total) / imagesPerPack); + for (auto it = m_batchPending.constBegin(); it != m_batchPending.constEnd(); ++it) { + DownloadQueryGroup b = it.value(); total += b.total; - int lastPageImages = b.total % imagesPerPack; - if (lastPageImages == 0) - lastPageImages = imagesPerPack; - - Downloader *previous = nullptr; - for (int i = 0; i < packs; ++i) - { - Downloader *downloader = new Downloader(m_profile, - b.tags.split(' '), - b.postFiltering, - QList() << b.site, - b.page + i * pagesPerPack, - (i == packs - 1 ? lastPageImages : imagesPerPack), - b.perpage, - b.path, - b.filename, - nullptr, - nullptr, - b.getBlacklisted, - m_profile->getBlacklist(), - false, - 0, - "", - previous); - downloader->setData(j.key()); - downloader->setQuit(false); - - connect(downloader, &Downloader::finishedImages, this, &DownloadsTab::getAllFinishedImages); - connect(downloader, &Downloader::finishedImagesPage, this, &DownloadsTab::getAllFinishedPage); - - m_waitingDownloaders.enqueue(downloader); - previous = downloader; - } + auto packLoader = new PackLoader(m_profile, b, usePacking ? imagesPerPack : -1, this); + connect(packLoader, &PackLoader::finishedPage, this, &DownloadsTab::getAllFinishedPage); + m_waitingPackLoaders.enqueue(packLoader); } m_getAllImagesCount = total; @@ -759,18 +751,23 @@ void DownloadsTab::getAllFinishedLogins() void DownloadsTab::getNextPack() { - m_downloaders.clear(); - - // If there are pending packs - if (!m_waitingDownloaders.isEmpty()) - { - m_downloaders.append(m_waitingDownloaders.dequeue()); + // If the current pack loader is not finished + if (m_currentPackLoader != nullptr && m_currentPackLoader->hasNext()) { getAllGetPages(); } + // If there are pending packs + else if (!m_waitingPackLoaders.isEmpty()) { + if (m_currentPackLoader != nullptr) { + m_currentPackLoader->deleteLater(); + } + m_currentPackLoader = m_waitingPackLoaders.dequeue(); + m_currentPackLoader->start(); + + getAllGetPages(); + } // Only images to download - else - { + else { m_batchAutomaticRetries = m_settings->value("Save/automaticretries", 0).toInt(); getAllImages(); } @@ -781,18 +778,15 @@ void DownloadsTab::getAllGetPages() m_progressDialog->clearImages(); m_progressDialog->setText(tr("Downloading pages, please wait...")); - int max = 0; - int packSize = 0; - for (Downloader *downloader : qAsConst(m_downloaders)) - { - downloader->getImages(); - max += downloader->pagesCount(); - packSize += downloader->imagesMax(); - } + const auto &query = m_currentPackLoader->query(); + const int images = m_currentPackLoader->nextPackSize(); + const int pages = qMax(1, qCeil(static_cast(images) / query.perpage)); m_progressDialog->setCurrentValue(0); - m_progressDialog->setCurrentMax(max); - m_batchCurrentPackSize = packSize; + m_progressDialog->setCurrentMax(pages); + m_batchCurrentPackSize = images; + + getAllFinishedImages(m_currentPackLoader->next()); } /** @@ -814,33 +808,37 @@ void DownloadsTab::getAllFinishedPage(Page *page) */ void DownloadsTab::getAllFinishedImages(const QList> &images) { - auto *downloader = qobject_cast(sender()); - m_downloaders.removeAll(downloader); - m_getAllIgnoredPre += downloader->ignoredCount(); - - const int row = downloader->getData().toInt(); + int row = -1; + for (auto it = m_batchPending.constBegin(); it != m_batchPending.constEnd(); ++it) { + if (it.value() == m_currentPackLoader->query()) { + row = it.key(); + break; + } + } + if (row < 0) { + log("Images received from unknown batch", Logger::Error); + return; + } - for (const auto &img : images) - { + for (const auto &img : images) { BatchDownloadImage d; d.image = img; d.queryGroup = &m_batchPending[row]; m_getAllRemaining.append(d); } - m_progressBars[row]->setValue(0); - m_progressBars[row]->setMaximum(images.count()); + // Ignore for aborted/resumed calls (partial packs) + if (m_getAll && !images.isEmpty()) { + m_progressBars[row]->setValue(0); + m_progressBars[row]->setMaximum(images.count()); - if (m_lastDownloader != nullptr) - { m_lastDownloader->deleteLater(); } - m_lastDownloader = downloader; - - // Update image to take into account unlisted images - int unlisted = m_batchCurrentPackSize - images.count(); - m_getAllImagesCount -= unlisted; + // Update image to take into account unlisted images + int unlisted = m_batchCurrentPackSize - images.count(); + m_getAllImagesCount -= unlisted; + } - if (m_downloaders.isEmpty()) - { + // Stop here if we're paused + if (m_getAll) { m_batchAutomaticRetries = m_settings->value("Save/automaticretries", 0).toInt(); getAllImages(); } @@ -851,18 +849,13 @@ void DownloadsTab::getAllFinishedImages(const QList> &imag */ void DownloadsTab::getAllImages() { - // Si la limite d'images est dépassée, on retire celles en trop - while (m_getAllRemaining.count() > m_getAllLimit && !m_getAllRemaining.isEmpty()) - m_getAllRemaining.takeLast().image->deleteLater(); - log(QStringLiteral("All images' urls have been received (%1).").arg(m_getAllRemaining.count()), Logger::Info); // We add the images to the download dialog m_progressDialog->clearImages(); m_progressDialog->setText(tr("Preparing images, please wait...")); m_progressDialog->setCount(m_getAllRemaining.count()); - for (const BatchDownloadImage &download : qAsConst(m_getAllRemaining)) - { + for (const BatchDownloadImage &download : qAsConst(m_getAllRemaining)) { const int siteId = download.siteId(m_groupBatchs); QSharedPointer img = download.image; @@ -877,25 +870,26 @@ void DownloadsTab::getAllImages() m_progressDialog->setText(tr("Downloading images...")); m_progressDialog->setCurrentValue(0); m_progressDialog->setCurrentMax(m_getAllRemaining.count()); - m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors); + m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors + m_getAllResumed); m_progressDialog->setTotalMax(m_getAllImagesCount); // We start the simultaneous downloads int count = qMax(1, qMin(m_settings->value("Save/simultaneous").toInt(), 10)); m_getAllCurrentlyProcessing.store(count); - for (int i = 0; i < count; i++) + for (int i = 0; i < count; i++) { _getAll(); + } } void DownloadsTab::_getAll() { // We quit as soon as the user cancels - if (m_progressDialog->cancelled()) + if (m_progressDialog->cancelled()) { return; + } // If there are still images do download - if (!m_getAllRemaining.empty()) - { + if (!m_getAllRemaining.empty()) { // We take the first image to download BatchDownloadImage download = m_getAllRemaining.takeFirst(); m_getAllDownloading.append(download); @@ -903,10 +897,10 @@ void DownloadsTab::_getAll() int siteId = download.siteId(m_groupBatchs); getAllGetImage(download, siteId); } - // When the batch download finishes - else if (m_getAllCurrentlyProcessing.fetchAndAddRelaxed(-1) == 1 && m_getAll) - { getAllFinished(); } + else if (m_getAllCurrentlyProcessing.fetchAndAddRelaxed(-1) == 1 && m_getAll) { + getAllFinished(); + } } void DownloadsTab::getAllImageOk(const BatchDownloadImage &download, int siteId, bool retry) @@ -914,22 +908,27 @@ void DownloadsTab::getAllImageOk(const BatchDownloadImage &download, int siteId, m_downloadTime.remove(download.image->url()); m_downloadTimeLast.remove(download.image->url()); - if (retry) + if (retry) { return; + } m_progressDialog->setCurrentValue(m_progressDialog->currentValue() + 1); - m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors); + m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors + m_getAllResumed); - if (siteId >= 0) - { + if (siteId >= 0) { int row = getRowForSite(siteId); m_progressBars[siteId - 1]->setValue(m_progressBars[siteId - 1]->value() + 1); - if (m_progressBars[siteId - 1]->value() >= m_progressBars[siteId - 1]->maximum()) - { ui->tableBatchGroups->item(row, 0)->setIcon(getIcon(":/images/status/ok.png")); } + if (m_progressBars[siteId - 1]->value() >= m_progressBars[siteId - 1]->maximum()) { + ui->tableBatchGroups->item(row, 0)->setIcon(getIcon(":/images/status/ok.png")); + } + + m_groupBatchs[siteId - 1].progressVal++; + m_batchPending[siteId - 1].progressVal++; } m_getAllDownloading.removeAll(download); - _getAll(); + QCoreApplication::processEvents(); + QTimer::singleShot(0, this, SLOT(_getAll())); } void DownloadsTab::imageUrlChanged(const QUrl &before, const QUrl &after) @@ -943,11 +942,11 @@ void DownloadsTab::getAllProgress(const QSharedPointer &img, qint64 bytes { const QUrl url = img->url(); - if (!m_downloadTimeLast.contains(url)) + if (!m_downloadTimeLast.contains(url)) { return; + } - if (m_downloadTimeLast[url].elapsed() >= 1000) - { + if (m_downloadTimeLast[url].elapsed() >= 200 && bytesReceived > 0) { m_downloadTimeLast[url].restart(); const int elapsed = m_downloadTime[url].elapsed(); const double speed = elapsed != 0 ? (bytesReceived * 1000) / elapsed : 0; @@ -955,8 +954,7 @@ void DownloadsTab::getAllProgress(const QSharedPointer &img, qint64 bytes } int percent = 0; - if (bytesTotal > 0) - { + if (bytesTotal > 0) { const qreal pct = static_cast(bytesReceived) / static_cast(bytesTotal); percent = qFloor(pct * 100); } @@ -975,20 +973,25 @@ void DownloadsTab::getAllGetImage(const BatchDownloadImage &download, int siteId QSharedPointer img = download.image; // If there is already a downloader for this image, we simply restart it - if (m_getAllImageDownloaders.contains(img)) - { + if (m_getAllImageDownloaders.contains(img)) { m_getAllImageDownloaders[img]->save(); return; } + // Stop here if we're paused + if (!m_getAll) { + return; + } + // Row int row = getRowForSite(siteId); // Path QString filename = download.query()->filename; QString path = download.query()->path; - if (siteId >= 0) - { ui->tableBatchGroups->item(row, 0)->setIcon(getIcon(":/images/status/downloading.png")); } + if (siteId >= 0) { + ui->tableBatchGroups->item(row, 0)->setIcon(getIcon(":/images/status/downloading.png")); + } // Track download progress m_progressDialog->loadingImage(img->url()); @@ -999,16 +1002,19 @@ void DownloadsTab::getAllGetImage(const BatchDownloadImage &download, int siteId // Start loading and saving image log(QStringLiteral("Loading image from `%1` %2").arg(img->fileUrl().toString()).arg(m_getAllDownloading.size()), Logger::Info); - int count = m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors + 1; + int count = m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors + m_getAllResumed + 1; bool getBlacklisted = download.queryGroup == nullptr || download.queryGroup->getBlacklisted; - auto imgDownloader = new ImageDownloader(m_profile, img, filename, path, count, true, false, getBlacklisted, this); + auto imgDownloader = new ImageDownloader(m_profile, img, filename, path, count, true, false, this); + if (!getBlacklisted) { + imgDownloader->setBlacklist(&m_profile->getBlacklist()); + } connect(imgDownloader, &ImageDownloader::saved, this, &DownloadsTab::getAllGetImageSaved, Qt::UniqueConnection); connect(imgDownloader, &ImageDownloader::downloadProgress, this, &DownloadsTab::getAllProgress, Qt::UniqueConnection); - imgDownloader->save(); m_getAllImageDownloaders[img] = imgDownloader; + imgDownloader->save(); } -void DownloadsTab::getAllGetImageSaved(const QSharedPointer &img, QMap result) +void DownloadsTab::getAllGetImageSaved(const QSharedPointer &img, QList result) { // Delete ImageDownloader to prevent leaks m_getAllImageDownloaders[img]->deleteLater(); @@ -1016,11 +1022,12 @@ void DownloadsTab::getAllGetImageSaved(const QSharedPointer &img, QMap &img, QMapisPaused()) - { + if (!m_progressDialog->isPaused()) { m_progressDialog->pause(); bool isDriveFull; QString drive; #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) - QDir destinationDir = QFileInfo(path).absoluteDir(); + QDir destinationDir = QFileInfo(re.path).absoluteDir(); QStorageInfo storage(destinationDir); isDriveFull = storage.isValid() && (storage.bytesAvailable() < img->fileSize() || storage.bytesAvailable() < 20 * 1024 * 1024); QString rootPath = storage.rootPath(); @@ -1059,28 +1062,28 @@ void DownloadsTab::getAllGetImageSaved(const QSharedPointer &img, QMaploadedImage(img->url(), res); @@ -1092,18 +1095,12 @@ void DownloadsTab::getAllCancel() { log(QStringLiteral("Cancelling downloads..."), Logger::Info); m_progressDialog->cancel(); - for (const BatchDownloadImage &download : qAsConst(m_getAllDownloading)) - { - download.image->abortTags(); + if (m_currentPackLoader != nullptr) { + m_currentPackLoader->abort(); } - for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) - { + for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) { it.value()->abort(); } - for (Downloader *downloader : qAsConst(m_downloaders)) - { - downloader->cancel(); - } m_getAll = false; ui->widgetDownloadButtons->setEnabled(true); DONE(); @@ -1114,30 +1111,25 @@ void DownloadsTab::getAllSkip() log(QStringLiteral("Skipping downloads..."), Logger::Info); int count = m_getAllDownloading.count(); - for (const BatchDownloadImage &download : qAsConst(m_getAllDownloading)) - { - download.image->abortTags(); - } - for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) - { + for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) { it.value()->abort(); } m_getAllSkippedImages.append(m_getAllDownloading); m_getAllDownloading.clear(); m_getAllSkipped += count; - m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors); + m_progressDialog->setTotalValue(m_getAllDownloaded + m_getAllExists + m_getAllIgnored + m_getAllErrors + m_getAllResumed); m_getAllCurrentlyProcessing.store(count); - for (int i = 0; i < count; ++i) + for (int i = 0; i < count; ++i) { _getAll(); + } DONE(); } void DownloadsTab::getAllFinished() { - if (!m_waitingDownloaders.isEmpty()) - { + if (!m_waitingPackLoaders.isEmpty() || (m_currentPackLoader != nullptr && m_currentPackLoader->hasNext())) { getNextPack(); return; } @@ -1146,30 +1138,32 @@ void DownloadsTab::getAllFinished() m_progressDialog->setTotalValue(m_progressDialog->totalMax()); // Delete objects - if (m_lastDownloader != nullptr) - { - m_lastDownloader->deleteLater(); - m_lastDownloader = nullptr; + if (m_currentPackLoader != nullptr) { + m_currentPackLoader->deleteLater(); + m_currentPackLoader = nullptr; } // Retry in case of error int failedCount = m_getAllErrors + m_getAllSkipped; - if (failedCount > 0) - { + if (failedCount > 0) { int reponse; - if (m_batchAutomaticRetries > 0) - { + if (m_batchAutomaticRetries > 0) { m_batchAutomaticRetries--; reponse = QMessageBox::Yes; - } - else - { - int totalCount = m_getAllDownloaded + m_getAllIgnored + m_getAllExists + m_getAll404s + m_getAllErrors + m_getAllSkipped; + } else { + // Trigger minor end actions on retry + switch (m_progressDialog->endAction()) + { + case 2: openTray(); break; + case 4: QSound::play(":/sounds/finished.wav"); break; + } + activateWindow(); + + int totalCount = m_getAllDownloaded + m_getAllIgnored + m_getAllExists + m_getAll404s + m_getAllErrors + m_getAllSkipped + m_getAllResumed; reponse = QMessageBox::question(this, tr("Getting images"), tr("Errors occured during the images download. Do you want to restart the download of those images? (%1/%2)").arg(failedCount).arg(totalCount), QMessageBox::Yes | QMessageBox::No); } - if (reponse == QMessageBox::Yes) - { + if (reponse == QMessageBox::Yes) { m_getAll = true; m_progressDialog->clear(); m_getAllRemaining.clear(); @@ -1185,6 +1179,7 @@ void DownloadsTab::getAllFinished() m_getAll404s = 0; m_getAllErrors = 0; m_getAllSkipped = 0; + m_getAllResumed = 0; m_progressDialog->show(); getAllImages(); return; @@ -1196,30 +1191,35 @@ void DownloadsTab::getAllFinished() this, tr("Getting images"), QString( - tr("%n file(s) downloaded successfully.", "", m_getAllDownloaded)+"\r\n"+ - tr("%n file(s) ignored.", "", m_getAllIgnored + m_getAllIgnoredPre)+"\r\n"+ - tr("%n file(s) already existing.", "", m_getAllExists)+"\r\n"+ - tr("%n file(s) not found on the server.", "", m_getAll404s)+"\r\n"+ - tr("%n file(s) skipped.", "", m_getAllSkipped)+"\r\n"+ + tr("%n file(s) downloaded successfully.", "", m_getAllDownloaded) + "\r\n" + + tr("%n file(s) ignored.", "", m_getAllIgnored + m_getAllIgnoredPre) + "\r\n" + + tr("%n file(s) already existing.", "", m_getAllExists) + "\r\n" + + tr("%n file(s) not found on the server.", "", m_getAll404s) + "\r\n" + + tr("%n file(s) skipped.", "", m_getAllSkipped) + "\r\n" + + tr("%n file(s) skipped from a previous download.", "", m_getAllResumed) + "\r\n" + tr("%n error(s).", "", m_getAllErrors) ) ); + // Mark downloads as finished + for (const int b : m_batchDownloading) { + m_groupBatchs[b].progressFinished = true; + } + // Final action switch (m_progressDialog->endAction()) { - case 1: m_progressDialog->close(); break; - case 2: openTray(); break; - case 3: m_parent->saveFolder(); break; - case 4: QSound::play(":/sounds/finished.wav"); break; - case 5: shutDown(); break; + case 1: m_progressDialog->close(); break; + case 2: openTray(); break; + case 3: m_parent->saveFolder(); break; + case 4: QSound::play(":/sounds/finished.wav"); break; + case 5: shutDown(); break; } activateWindow(); m_getAll = false; // Remove after download and retries are finished - if (m_progressDialog->endRemove()) - { + if (m_progressDialog->endRemove()) { batchRemoveGroups(m_batchDownloading.toList()); batchRemoveUniques(m_batchUniqueDownloading.toList()); } @@ -1232,27 +1232,25 @@ void DownloadsTab::getAllFinished() void DownloadsTab::getAllPause() { - if (m_progressDialog->isPaused()) - { + if (m_progressDialog->isPaused()) { log(QStringLiteral("Pausing downloads..."), Logger::Info); - for (const auto &download : qAsConst(m_getAllDownloading)) - { - download.image->abortTags(); + m_getAll = false; + if (m_currentPackLoader != nullptr) { + m_currentPackLoader->abort(); } - for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) - { + for (auto it = m_getAllImageDownloaders.constBegin(); it != m_getAllImageDownloaders.constEnd(); ++it) { it.value()->abort(); } - m_getAll = false; - } - else - { + } else { log(QStringLiteral("Recovery of downloads..."), Logger::Info); - for (const auto &download : qAsConst(m_getAllDownloading)) - { - getAllGetImage(download, download.siteId(m_groupBatchs)); - } m_getAll = true; + if (m_getAllDownloading.isEmpty()) { + getAllFinishedImages(QList>()); + } else { + for (const auto &download : qAsConst(m_getAllDownloading)) { + getAllGetImage(download, download.siteId(m_groupBatchs)); + } + } } DONE(); } diff --git a/gui/src/tabs/downloads-tab.h b/gui/src/tabs/downloads-tab.h index 535a04ca3..b978a18b7 100644 --- a/gui/src/tabs/downloads-tab.h +++ b/gui/src/tabs/downloads-tab.h @@ -18,10 +18,11 @@ namespace Ui class BatchDownloadImage; class BatchWindow; -class Downloader; class DownloadQueryGroup; class DownloadQueryImage; class ImageDownloader; +struct ImageSaveResult; +class PackLoader; class Page; class Profile; class MainWindow; @@ -54,7 +55,7 @@ class DownloadsTab : public QWidget void addUnique(); void batchAddGroup(const DownloadQueryGroup &values); void batchAddUnique(const DownloadQueryImage &query, bool save = true); - void addTableItem(QTableWidget *table, int row, int col, const QString &text); + QTableWidgetItem *addTableItem(QTableWidget *table, int row, int col, const QString &text); // Update void updateBatchGroups(int, int); @@ -73,7 +74,7 @@ class DownloadsTab : public QWidget void getAllFinishedImages(const QList> &images); void getAllImages(); void getAllGetImage(const BatchDownloadImage &download, int siteId); - void getAllGetImageSaved(const QSharedPointer &img, QMap result); + void getAllGetImageSaved(const QSharedPointer &img, QList result); void getAllProgress(const QSharedPointer &img, qint64 bytesReceived, qint64 bytesTotal); void getAllCancel(); void getAllPause(); @@ -103,7 +104,7 @@ class DownloadsTab : public QWidget QSettings *m_settings; MainWindow *m_parent; - int m_getAllDownloaded, m_getAllExists, m_getAllIgnored, m_getAllIgnoredPre, m_getAll404s, m_getAllErrors, m_getAllSkipped, m_getAllLimit; + int m_getAllDownloaded, m_getAllExists, m_getAllIgnored, m_getAllIgnoredPre, m_getAll404s, m_getAllErrors, m_getAllSkipped, m_getAllResumed, m_getAllLimit; bool m_allow, m_getAll; BatchWindow *m_progressDialog; QMap m_downloadTime; @@ -117,9 +118,8 @@ class DownloadsTab : public QWidget QList m_getAllRemaining, m_getAllDownloading, m_getAllFailed, m_getAllSkippedImages; QMap, ImageDownloader*> m_getAllImageDownloaders; QMap m_icons; - QList m_downloaders; - Downloader *m_lastDownloader; - QQueue m_waitingDownloaders; + QQueue m_waitingPackLoaders; + PackLoader *m_currentPackLoader = nullptr; QList m_getAllLogins; int m_batchAutomaticRetries, m_getAllImagesCount, m_batchCurrentPackSize; QAtomicInt m_getAllCurrentlyProcessing; diff --git a/gui/src/tabs/downloads-tab.ui b/gui/src/tabs/downloads-tab.ui index 64590101f..b9d9a5975 100644 --- a/gui/src/tabs/downloads-tab.ui +++ b/gui/src/tabs/downloads-tab.ui @@ -101,6 +101,11 @@ Get blacklisted + + + Galleries count as one + + Progress diff --git a/gui/src/tabs/favorites-tab.cpp b/gui/src/tabs/favorites-tab.cpp index a8eaf3820..5072716df 100644 --- a/gui/src/tabs/favorites-tab.cpp +++ b/gui/src/tabs/favorites-tab.cpp @@ -8,6 +8,7 @@ #include "favorite-window.h" #include "functions.h" #include "helpers.h" +#include "logger.h" #include "main-window.h" #include "models/favorite.h" #include "models/page.h" @@ -31,8 +32,7 @@ FavoritesTab::FavoritesTab(Profile *profile, MainWindow *parent) const int vSpace = m_settings->value("Margins/vertical", 6).toInt(); m_favoritesLayout = new FixedSizeGridLayout(hSpace, vSpace); const bool fixedWidthLayout = m_settings->value("resultsFixedWidthLayout", false).toBool(); - if (fixedWidthLayout) - { + if (fixedWidthLayout) { const int borderSize = m_settings->value("borders", 3).toInt(); const qreal upscale = m_settings->value("thumbnailUpscale", 1.0).toDouble(); m_favoritesLayout->setFixedWidth(qFloor(FAVORITES_THUMB_SIZE * upscale + borderSize * 2)); @@ -113,14 +113,16 @@ void FavoritesTab::updateFavorites() const QString &order = assoc[ui->comboOrder->currentIndex()]; const bool reverse = (ui->comboAsc->currentIndex() == 1); - if (order == "note") - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByNote); } - else if (order == "lastviewed") - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByLastViewed); } - else - { std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByName); } - if (reverse) - { m_favorites = reversed(m_favorites); } + if (order == "note") { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByNote); + } else if (order == "lastviewed") { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByLastViewed); + } else { + std::sort(m_favorites.begin(), m_favorites.end(), Favorite::sortByName); + } + if (reverse) { + m_favorites = reversed(m_favorites); + } const QString format = tr("MM/dd/yyyy"); clearLayout(m_favoritesLayout); @@ -130,8 +132,7 @@ void FavoritesTab::updateFavorites() const int borderSize = m_settings->value("borders", 3).toInt(); const int dim = qFloor(FAVORITES_THUMB_SIZE * upscale + borderSize * 2); - for (Favorite &fav : m_favorites) - { + for (Favorite &fav : m_favorites) { const QString xt = tr("Name: %1
Note: %2 %
Last view: %3").arg(fav.getName(), QString::number(fav.getNote()), fav.getLastViewed().toString(format)); QWidget *w = new QWidget(ui->scrollAreaWidgetContents); auto *l = new QVBoxLayout; @@ -140,17 +141,14 @@ void FavoritesTab::updateFavorites() int maxNewImages = 0; bool precise = true; - for (const Monitor &monitor : qAsConst(fav.getMonitors())) - { - if (monitor.cumulated() > maxNewImages) - { + for (const Monitor &monitor : qAsConst(fav.getMonitors())) { + if (monitor.cumulated() > maxNewImages) { maxNewImages = monitor.cumulated(); precise = monitor.preciseCumulated(); } } - if (display.contains("i")) - { + if (display.contains("i")) { const bool resizeInsteadOfCropping = m_settings->value("resizeInsteadOfCropping", true).toBool(); QPixmap img = fav.getImage(); @@ -170,14 +168,15 @@ void FavoritesTab::updateFavorites() } QString label; - if (display.contains("n")) - { + if (display.contains("n")) { label += fav.getName(); - if (maxNewImages > 0 && !display.contains("i")) - { label += QStringLiteral(" (%1%2)").arg(maxNewImages).arg(!precise ? "+" : QString()); } + if (maxNewImages > 0 && !display.contains("i")) { + label += QStringLiteral(" (%1%2)").arg(maxNewImages).arg(!precise ? "+" : QString()); + } + } + if (display.contains("d")) { + label += "
(" + QString::number(fav.getNote()) + " % - " + fav.getLastViewed().toString(format) + ")"; } - if (display.contains("d")) - { label += "
(" + QString::number(fav.getNote()) + " % - " + fav.getLastViewed().toString(format) + ")"; } QAffiche *caption = new QAffiche(fav.getName(), 0, QColor(), this); caption->setText(label); @@ -185,8 +184,9 @@ void FavoritesTab::updateFavorites() caption->setAlignment(Qt::AlignCenter); caption->setToolTip(xt); caption->setFixedWidth(dim); - if (!caption->text().isEmpty()) - { + if (!caption->text().isEmpty()) { + connect(caption, SIGNAL(rightClicked(QString)), this, SLOT(favoriteProperties(QString))); + connect(caption, SIGNAL(middleClicked(QString)), m_parent, SLOT(addTab(QString))); connect(caption, SIGNAL(clicked(QString)), this, SLOT(loadFavorite(QString))); l->addWidget(caption); } @@ -205,7 +205,8 @@ void FavoritesTab::load() bool FavoritesTab::validateImage(const QSharedPointer &img, QString &error) { - return (img->createdAt() > m_loadFavorite || img->createdAt().isNull()) && SearchTab::validateImage(img, error); + bool dateOk = img->createdAt() > m_loadFavorite || img->createdAt().isNull(); + return dateOk && SearchTab::validateImage(img, error); } void FavoritesTab::write(QJsonObject &json) const @@ -229,60 +230,46 @@ void FavoritesTab::setPageLabelText(QLabel *txt, Page *page, const QListisChecked()) - { actuals.append(keys.at(i)); } - } const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); - for (int i = 0; i < actuals.count(); i++) - { - const auto &page = m_pages[actuals[i]].first(); - const QString search = m_currentTags + " " + m_settings->value("add").toString().toLower().trimmed(); + + QList> pages = this->getPagesToDownload(); + for (const QSharedPointer &page : pages) { + const QStringList search = (m_currentTags + " " + m_settings->value("add").toString().toLower().trimmed()).split(' ', QString::SkipEmptyParts); const int perpage = unloaded ? ui->spinImagesPerPage->value() : page->pageImageCount(); - const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); + const QStringList postFiltering = (m_postFiltering->toPlainText() + " " + m_settings->value("globalPostFilter").toString()).split(' ', QString::SkipEmptyParts); - emit batchAddGroup(DownloadQueryGroup(m_settings, search, ui->spinPage->value(), perpage, perpage, postFiltering, m_sites.value(actuals.at(i)))); + emit batchAddGroup(DownloadQueryGroup(m_settings, search, ui->spinPage->value(), perpage, perpage, postFiltering, page->site())); } } void FavoritesTab::getAll() { - QStringList actuals, keys = m_sites.keys(); - for (int i = 0; i < m_checkboxes.count(); i++) - { - if (m_checkboxes.at(i)->isChecked()) - actuals.append(keys.at(i)); - } - - for (const QString &actual : actuals) - { - const auto &page = m_pages[actual].first(); - + QList> pages = this->getPagesToDownload(); + for (const QSharedPointer &page : pages) { const int highLimit = page->highLimit(); const int currentCount = page->pageImageCount(); const int imageCount = page->imagesCount() >= 0 ? page->imagesCount() : page->maxImagesCount(); const int total = imageCount > 0 ? qMax(currentCount, imageCount) : (highLimit > 0 ? highLimit : currentCount); const int perPage = highLimit > 0 ? (imageCount > 0 ? qMin(highLimit, imageCount) : highLimit) : currentCount; - if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) + if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) { continue; + } - const QString search = m_currentTags + " " + m_settings->value("add").toString().toLower().trimmed(); - const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); - Site *site = m_sites.value(actual); + const QStringList search = (m_currentTags + " " + m_settings->value("add").toString().toLower().trimmed()).split(' ', QString::SkipEmptyParts); + const QStringList postFiltering = (m_postFiltering->toPlainText() + " " + m_settings->value("globalPostFilter").toString()).split(' ', QString::SkipEmptyParts); - emit batchAddGroup(DownloadQueryGroup(m_settings, search, 1, perPage, total, postFiltering, site)); + emit batchAddGroup(DownloadQueryGroup(m_settings, search, 1, perPage, total, postFiltering, page->site())); } } @@ -296,8 +283,9 @@ QString FavoritesTab::tags() const void FavoritesTab::loadFavorite(const QString &name) { const int index = name.isEmpty() ? m_currentFav : m_favorites.indexOf(Favorite(name)); - if (index < 0) + if (index < 0) { return; + } Favorite fav = m_favorites[index]; m_currentTags = fav.getName(); @@ -315,8 +303,9 @@ void FavoritesTab::checkFavorites() } void FavoritesTab::loadNextFavorite() { - if (m_currentFav + 1 >= m_favorites.count()) + if (m_currentFav + 1 >= m_favorites.count()) { return; + } m_currentFav++; m_currentTags = m_favorites[m_currentFav].getName(); @@ -326,17 +315,16 @@ void FavoritesTab::loadNextFavorite() } void FavoritesTab::viewed() { - if (m_currentTags.isEmpty()) - { + if (m_currentTags.isEmpty()) { const int reponse = QMessageBox::question(this, tr("Mark as viewed"), tr("Are you sure you want to mark all your favorites as viewed?"), QMessageBox::Yes | QMessageBox::No); - if (reponse == QMessageBox::Yes) - { - for (const Favorite &fav : qAsConst(m_favorites)) - { setFavoriteViewed(fav.getName()); } + if (reponse == QMessageBox::Yes) { + for (const Favorite &fav : qAsConst(m_favorites)) { + setFavoriteViewed(fav.getName()); + } } + } else { + setFavoriteViewed(m_currentTags); } - else - { setFavoriteViewed(m_currentTags); } m_profile->emitFavorite(); } @@ -345,14 +333,16 @@ void FavoritesTab::setFavoriteViewed(const QString &tag) log(QStringLiteral("Marking \"%1\" as viewed...").arg(tag)); const int index = tag.isEmpty() ? m_currentFav : m_favorites.indexOf(Favorite(tag)); - if (index < 0) + if (index < 0) { return; + } Favorite &fav = m_favorites[index]; fav.setLastViewed(QDateTime::currentDateTime()); - for (Monitor &monitor : fav.getMonitors()) + for (Monitor &monitor : fav.getMonitors()) { monitor.setCumulated(0, true); + } DONE(); } @@ -362,8 +352,7 @@ void FavoritesTab::favoritesBack() ui->widgetFavorites->show(); ui->splitter->setSizes(QList() << 1 << 0); - if (!m_currentTags.isEmpty() || m_currentFav != -1) - { + if (!m_currentTags.isEmpty() || m_currentFav != -1) { m_currentTags = QString(); m_currentFav = -1; ui->widgetFavorites->show(); @@ -372,8 +361,9 @@ void FavoritesTab::favoritesBack() void FavoritesTab::favoriteProperties(const QString &name) { const int index = name.isEmpty() ? m_currentFav : m_favorites.indexOf(Favorite(name)); - if (index < 0) + if (index < 0) { return; + } const Favorite fav = m_favorites[index]; auto fwin = new FavoriteWindow(m_profile, fav, this); @@ -381,17 +371,14 @@ void FavoritesTab::favoriteProperties(const QString &name) } void FavoritesTab::focusSearch() -{ } +{} void FavoritesTab::changeEvent(QEvent *event) { // Automatically retranslate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); - setWindowTitle(tr("Favorites")); - emit titleChanged(this); } QWidget::changeEvent(event); @@ -399,5 +386,21 @@ void FavoritesTab::changeEvent(QEvent *event) void FavoritesTab::updateTitle() { - // No-op, the Favorites tab never changes its title + setWindowTitle(tr("Favorites") + (m_currentTags.isEmpty() ? "" : " - " + m_currentTags)); + emit titleChanged(this); +} + +void FavoritesTab::splitterMoved(int pos, int index) +{ + const QString title = tr("Favorites"); + + int min, max; + ui->splitter->getRange(index, &min, &max); + + if (index == 1 && pos >= max) { + setWindowTitle(title); + emit titleChanged(this); + } else if (windowTitle() == title) { + updateTitle(); + } } diff --git a/gui/src/tabs/favorites-tab.h b/gui/src/tabs/favorites-tab.h index 40ce1c9ab..97c05c41d 100644 --- a/gui/src/tabs/favorites-tab.h +++ b/gui/src/tabs/favorites-tab.h @@ -52,6 +52,7 @@ class FavoritesTab : public SearchTab void addResultsPage(Page *page, const QList> &imgs, bool merged, const QString &noResultsMessage = nullptr) override; void setPageLabelText(QLabel *txt, Page *page, const QList> &imgs, const QString &noResultsMessage = nullptr) override; void updateTitle() override; + void splitterMoved(int pos, int index); private: QDateTime m_loadFavorite; diff --git a/gui/src/tabs/favorites-tab.ui b/gui/src/tabs/favorites-tab.ui index fd02aa5c7..b3903ac5e 100644 --- a/gui/src/tabs/favorites-tab.ui +++ b/gui/src/tabs/favorites-tab.ui @@ -454,7 +454,7 @@ QWidget#scrollAreaWidgetContents { background-color:transparent; - + 0 @@ -467,6 +467,22 @@ QWidget#scrollAreaWidgetContents { 0 + + + + + + + Qt::Vertical + + + + 0 + 0 + + + +
@@ -926,6 +942,22 @@ QWidget#scrollAreaWidgetContents { + + splitter + splitterMoved(int,int) + FavoritesTab + splitterMoved(int,int) + + + 778 + 325 + + + 795 + 328 + + + load() @@ -945,5 +977,6 @@ QWidget#scrollAreaWidgetContents { favoritesBack() viewed() favoriteProperties() + splitterMoved(int,int) diff --git a/gui/src/tabs/gallery-tab.cpp b/gui/src/tabs/gallery-tab.cpp index 38605ffd6..9f972a036 100644 --- a/gui/src/tabs/gallery-tab.cpp +++ b/gui/src/tabs/gallery-tab.cpp @@ -6,7 +6,9 @@ #include "downloader/download-query-group.h" #include "logger.h" #include "main-window.h" +#include "models/image.h" #include "models/page.h" +#include "models/profile.h" #include "models/site.h" #include "search-window.h" #include "ui/text-edit.h" @@ -14,14 +16,13 @@ #define MAX_TAB_NAME_LENGTH 40 -GalleryTab::GalleryTab(Site *site, QString name, QString id, Profile *profile, MainWindow *parent) +GalleryTab::GalleryTab(Site *site, QSharedPointer gallery, Profile *profile, MainWindow *parent) : GalleryTab(profile, parent) { m_site = site; - m_name = std::move(name); - m_id = std::move(id); + m_gallery = std::move(gallery); - ui->labelGalleryName->setText(m_name); + ui->labelGalleryName->setText(m_gallery->name()); load(); } @@ -73,7 +74,7 @@ void GalleryTab::closeEvent(QCloseEvent *e) void GalleryTab::load() { updateTitle(); - loadTags(QStringList() << "gallery:" + m_id); + loadTags(m_gallery); } QList GalleryTab::loadSites() const @@ -85,9 +86,11 @@ QList GalleryTab::loadSites() const void GalleryTab::write(QJsonObject &json) const { + QJsonObject jsonGallery; + m_gallery->write(jsonGallery); + json["gallery"] = jsonGallery; + json["type"] = QStringLiteral("gallery"); - json["name"] = m_name; - json["id"] = m_id; json["site"] = m_site->url(); json["page"] = ui->spinPage->value(); json["perpage"] = ui->spinImagesPerPage->value(); @@ -97,11 +100,19 @@ void GalleryTab::write(QJsonObject &json) const bool GalleryTab::read(const QJsonObject &json, bool preload) { - m_name = json["name"].toString(); - m_id = json["id"].toString(); - m_site = m_sites[json["site"].toString()]; + const QString site = json["site"].toString(); + if (!m_sites.contains(site)) { + return false; + } + m_site = m_sites[site]; + + m_gallery = QSharedPointer(new Image()); + if (!m_gallery->read(json["gallery"].toObject(), m_profile->getSites())) { + m_gallery->deleteLater(); + return false; + } - ui->labelGalleryName->setText(m_name); + ui->labelGalleryName->setText(m_gallery->name()); ui->spinPage->setValue(json["page"].toInt()); ui->spinImagesPerPage->setValue(json["perpage"].toInt()); ui->spinColumns->setValue(json["columns"].toInt()); @@ -110,8 +121,9 @@ bool GalleryTab::read(const QJsonObject &json, bool preload) QJsonArray jsonPostFilters = json["postFiltering"].toArray(); QStringList postFilters; postFilters.reserve(jsonPostFilters.count()); - for (auto tag : jsonPostFilters) + for (auto tag : jsonPostFilters) { postFilters.append(tag.toString()); + } setPostFilter(postFilters.join(' ')); setTags("", preload); @@ -121,17 +133,24 @@ bool GalleryTab::read(const QJsonObject &json, bool preload) void GalleryTab::getPage() { + if (!m_pages.contains(m_site->url())) { + return; + } + const auto &page = m_pages[m_site->url()].first(); const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); const int perPage = unloaded ? ui->spinImagesPerPage->value() : page->pageImageCount(); - const QString tags = "gallery:" + m_id; const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); - emit batchAddGroup(DownloadQueryGroup(m_settings, tags, ui->spinPage->value(), perPage, perPage, postFiltering, m_site)); + emit batchAddGroup(DownloadQueryGroup(m_settings, m_gallery, ui->spinPage->value(), perPage, perPage, postFiltering, m_site)); } void GalleryTab::getAll() { + if (!m_pages.contains(m_site->url())) { + return; + } + const auto &page = m_pages[m_site->url()].first(); const int highLimit = page->highLimit(); @@ -139,13 +158,13 @@ void GalleryTab::getAll() const int imageCount = page->imagesCount() >= 0 ? page->imagesCount() : page->maxImagesCount(); const int total = imageCount > 0 ? qMax(currentCount, imageCount) : (highLimit > 0 ? highLimit : currentCount); const int perPage = highLimit > 0 ? (imageCount > 0 ? qMin(highLimit, imageCount) : highLimit) : currentCount; - if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) + if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) { return; + } - const QString search = "gallery:" + m_id; const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); - emit batchAddGroup(DownloadQueryGroup(m_settings, search, 1, perPage, total, postFiltering, m_site)); + emit batchAddGroup(DownloadQueryGroup(m_settings, m_gallery, 1, perPage, total, postFiltering, m_site)); } @@ -153,12 +172,12 @@ void GalleryTab::setTags(const QString &tags, bool preload) { Q_UNUSED(tags); - activateWindow(); - - if (preload) + if (preload) { + activateWindow(); load(); - else + } else { updateTitle(); + } } void GalleryTab::focusSearch() @@ -167,14 +186,13 @@ void GalleryTab::focusSearch() } QString GalleryTab::tags() const -{ return "gallery:" + m_id; } +{ return QString(); } void GalleryTab::changeEvent(QEvent *event) { // Automatically re-translate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } @@ -183,6 +201,7 @@ void GalleryTab::changeEvent(QEvent *event) void GalleryTab::updateTitle() { - setWindowTitle(m_name.length() > MAX_TAB_NAME_LENGTH ? m_name.left(MAX_TAB_NAME_LENGTH - 3) + "..." : m_name); + const QString &name = m_gallery->name(); + setWindowTitle(name.length() > MAX_TAB_NAME_LENGTH ? name.left(MAX_TAB_NAME_LENGTH - 3) + "..." : name); emit titleChanged(this); } diff --git a/gui/src/tabs/gallery-tab.h b/gui/src/tabs/gallery-tab.h index e5ea83497..c76d6d51f 100644 --- a/gui/src/tabs/gallery-tab.h +++ b/gui/src/tabs/gallery-tab.h @@ -2,6 +2,7 @@ #define GALLERY_TAB_H #include +#include #include "tabs/search-tab.h" @@ -11,15 +12,15 @@ namespace Ui } +class Image; class MainWindow; -class TextEdit; class GalleryTab : public SearchTab { Q_OBJECT public: - explicit GalleryTab(Site *site, QString name, QString id, Profile *profile, MainWindow *parent); + explicit GalleryTab(Site *site, QSharedPointer gallery, Profile *profile, MainWindow *parent); explicit GalleryTab(Profile *profile, MainWindow *parent); ~GalleryTab() override; Ui::GalleryTab *ui; @@ -45,8 +46,7 @@ class GalleryTab : public SearchTab private: Site *m_site; - QString m_name; - QString m_id; + QSharedPointer m_gallery; }; #endif // GALLERY_TAB_H diff --git a/gui/src/tabs/gallery-tab.ui b/gui/src/tabs/gallery-tab.ui index 0446f85da..450b40f9e 100644 --- a/gui/src/tabs/gallery-tab.ui +++ b/gui/src/tabs/gallery-tab.ui @@ -16,9 +16,6 @@ 0 - - New pool tab - :/images/icon.ico:/images/icon.ico diff --git a/gui/src/tabs/log-tab.cpp b/gui/src/tabs/log-tab.cpp index 7f7fd115b..e6c833637 100644 --- a/gui/src/tabs/log-tab.cpp +++ b/gui/src/tabs/log-tab.cpp @@ -13,10 +13,8 @@ LogTab::LogTab(QWidget *parent) // Load already written log QFile logFile(Logger::getInstance().logFile()); - if (logFile.open(QFile::ReadOnly | QFile::Text)) - { - while (!logFile.atEnd()) - { + if (logFile.open(QFile::ReadOnly | QFile::Text)) { + while (!logFile.atEnd()) { write(logFile.readLine()); } logFile.close(); @@ -48,8 +46,7 @@ void LogTab::write(const QString &msg) { "Error", "red" }, }; QString levelColor = colors[level]; - if (!levelColor.isEmpty()) - { + if (!levelColor.isEmpty()) { htmlMsg.insert(msg.size(), ""); htmlMsg.insert(timeEnd + 1, QString("").arg(colors[level])); } @@ -59,12 +56,12 @@ void LogTab::write(const QString &msg) htmlMsg.insert(0, ""); // Links color - static const QRegularExpression rxLinks("`(http[^']+)`"); + static const QRegularExpression rxLinks("`(http[^`]+)`"); htmlMsg.replace(rxLinks, R"(\1)"); // File paths color #ifdef Q_OS_WIN - static const QRegularExpression rxPaths("`(\\w:[\\\\/][^`]+)`"); + static const QRegularExpression rxPaths(R"(`(\w:[\\/][^`]+)`)"); #else static const QRegularExpression rxPaths("`(/[^`]+)`"); #endif @@ -77,8 +74,7 @@ void LogTab::write(const QString &msg) void LogTab::clear() { QFile logFile(Logger::getInstance().logFile()); - if (logFile.open(QFile::WriteOnly | QFile::Text)) - { + if (logFile.open(QFile::WriteOnly | QFile::Text)) { logFile.resize(0); logFile.close(); } @@ -94,8 +90,7 @@ void LogTab::open() void LogTab::changeEvent(QEvent *event) { // Automatically re-translate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } diff --git a/gui/src/tabs/pool-tab.cpp b/gui/src/tabs/pool-tab.cpp index 415d12efc..7c50af2dd 100644 --- a/gui/src/tabs/pool-tab.cpp +++ b/gui/src/tabs/pool-tab.cpp @@ -37,8 +37,9 @@ PoolTab::PoolTab(Profile *profile, MainWindow *parent) ui_scrollAreaResults = ui->scrollAreaResults; QStringList sources = m_sites.keys(); - for (const QString &source : sources) - { ui->comboSites->addItem(source); } + for (const QString &source : sources) { + ui->comboSites->addItem(source); + } // Search field m_search = createAutocomplete(); @@ -118,16 +119,18 @@ bool PoolTab::read(const QJsonObject &json, bool preload) QJsonArray jsonPostFilters = json["postFiltering"].toArray(); QStringList postFilters; postFilters.reserve(jsonPostFilters.count()); - for (auto tag : jsonPostFilters) + for (auto tag : jsonPostFilters) { postFilters.append(tag.toString()); + } setPostFilter(postFilters.join(' ')); // Tags QJsonArray jsonTags = json["tags"].toArray(); QStringList tags; tags.reserve(jsonTags.count()); - for (auto tag : jsonTags) + for (auto tag : jsonTags) { tags.append(tag.toString()); + } setTags(tags.join(' '), preload); return true; @@ -136,30 +139,42 @@ bool PoolTab::read(const QJsonObject &json, bool preload) void PoolTab::getPage() { - const auto &page = m_pages[ui->comboSites->currentText()].first(); - const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); + + const QString &ste = ui->comboSites->currentText(); + const auto &page = unloaded ? (m_pages.contains(ste) ? m_pages[ste].first() : QSharedPointer()) : m_pages.first().first(); + if (page.isNull()) { + return; + } + const int perPage = unloaded ? ui->spinImagesPerPage->value() : page->pageImageCount(); - const QString tags = "pool:" + QString::number(ui->spinPool->value()) + " " + m_search->toPlainText() + " " + m_settings->value("add").toString().trimmed(); - const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); + const QStringList tags = ("pool:" + QString::number(ui->spinPool->value()) + " " + m_search->toPlainText() + " " + m_settings->value("add").toString().trimmed()).split(' ', QString::SkipEmptyParts); + const QStringList postFiltering = (m_postFiltering->toPlainText() + " " + m_settings->value("globalPostFilter").toString()).split(' ', QString::SkipEmptyParts); Site *site = m_sites.value(ui->comboSites->currentText()); emit batchAddGroup(DownloadQueryGroup(m_settings, tags, ui->spinPage->value(), perPage, perPage, postFiltering, site)); } void PoolTab::getAll() { - const auto &page = m_pages[ui->comboSites->currentText()].first(); + const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); + + const QString &ste = ui->comboSites->currentText(); + const auto &page = unloaded ? (m_pages.contains(ste) ? m_pages[ste].first() : QSharedPointer()) : m_pages.first().first(); + if (page.isNull()) { + return; + } const int highLimit = page->highLimit(); const int currentCount = page->pageImageCount(); const int imageCount = page->imagesCount() >= 0 ? page->imagesCount() : page->maxImagesCount(); const int total = imageCount > 0 ? qMax(currentCount, imageCount) : (highLimit > 0 ? highLimit : currentCount); const int perPage = highLimit > 0 ? (imageCount > 0 ? qMin(highLimit, imageCount) : highLimit) : currentCount; - if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) + if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) { return; + } - const QString search = "pool:" + QString::number(ui->spinPool->value()) + " " + m_search->toPlainText() + " " + m_settings->value("add").toString().trimmed(); - const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); + const QStringList search = ("pool:" + QString::number(ui->spinPool->value()) + " " + m_search->toPlainText() + " " + m_settings->value("add").toString().trimmed()).split(' ', QString::SkipEmptyParts); + const QStringList postFiltering = (m_postFiltering->toPlainText() + " " + m_settings->value("globalPostFilter").toString()).split(' ', QString::SkipEmptyParts); Site *site = m_sites.value(ui->comboSites->currentText()); emit batchAddGroup(DownloadQueryGroup(m_settings, search, 1, perPage, total, postFiltering, site)); @@ -168,28 +183,31 @@ void PoolTab::getAll() void PoolTab::setTags(const QString &tags, bool preload) { - activateWindow(); m_search->setText(tags); - if (preload) + if (preload) { + activateWindow(); load(); - else + } else { updateTitle(); + } } void PoolTab::setPool(int id, const QString &site) { activateWindow(); ui->spinPool->setValue(id); const int index = ui->comboSites->findText(site); - if (index != -1) - { ui->comboSites->setCurrentIndex(index); } + if (index != -1) { + ui->comboSites->setCurrentIndex(index); + } load(); } void PoolTab::setSite(const QString &site) { const int index = ui->comboSites->findText(site); - if (index != -1) - { ui->comboSites->setCurrentIndex(index); } + if (index != -1) { + ui->comboSites->setCurrentIndex(index); + } } void PoolTab::focusSearch() @@ -204,8 +222,7 @@ QString PoolTab::tags() const void PoolTab::changeEvent(QEvent *event) { // Automatically re-translate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } diff --git a/gui/src/tabs/pool-tab.ui b/gui/src/tabs/pool-tab.ui index 31bf3202e..3b6072f3a 100644 --- a/gui/src/tabs/pool-tab.ui +++ b/gui/src/tabs/pool-tab.ui @@ -16,9 +16,6 @@ 0 - - New pool tab - :/images/icon.ico:/images/icon.ico diff --git a/gui/src/tabs/search-tab.cpp b/gui/src/tabs/search-tab.cpp index 49df71a94..15defa0a7 100644 --- a/gui/src/tabs/search-tab.cpp +++ b/gui/src/tabs/search-tab.cpp @@ -13,6 +13,7 @@ #include "functions.h" #include "helpers.h" #include "image-context-menu.h" +#include "logger.h" #include "main-window.h" #include "models/api/api.h" #include "models/favorite.h" @@ -32,7 +33,7 @@ SearchTab::SearchTab(Profile *profile, MainWindow *parent) - : QWidget(parent), m_profile(profile), m_lastPageMaxId(0), m_lastPageMinId(0), m_sites(profile->getSites()), m_favorites(profile->getFavorites()), m_parent(parent), m_settings(profile->getSettings()), m_pagemax(-1), m_stop(true), m_from_history(false), m_history_cursor(0), m_lastTags(QString()) + : QWidget(parent), m_profile(profile), m_lastPageMaxId(0), m_lastPageMinId(0), m_sites(profile->getSites()), m_favorites(profile->getFavorites()), m_parent(parent), m_settings(profile->getSettings()), m_pagemax(-1), m_stop(true), m_from_history(false), m_history_cursor(0) { setAttribute(Qt::WA_DeleteOnClose); @@ -40,25 +41,16 @@ SearchTab::SearchTab(Profile *profile, MainWindow *parent) m_checkboxesSignalMapper = new QSignalMapper(this); connect(m_checkboxesSignalMapper, SIGNAL(mapped(QString)), this, SLOT(toggleSource(QString))); - // Auto-complete list - m_completion.append(profile->getAutoComplete()); - m_completion.append(profile->getCustomAutoComplete()); - - // Favorite tags - m_completion.reserve(m_completion.count() + m_favorites.count()); - for (const Favorite &fav : qAsConst(m_favorites)) - m_completion.append(fav.getName()); - // Modifiers - for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) - { + for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) { Site *site = it.value(); const QStringList modifiers = site->getApis().first()->modifiers(); m_completion.append(modifiers); } - m_completion.removeDuplicates(); - m_completion.sort(); + + // Auto-complete list + m_completion.append(profile->getAutoComplete()); setSelectedSources(m_settings); } @@ -70,14 +62,17 @@ void SearchTab::init() const QString infinite = m_settings->value("infiniteScroll", "disabled").toString(); // Always hide scroll button before results are loaded - if (ui_buttonEndlessLoad != nullptr) + if (ui_buttonEndlessLoad != nullptr) { ui_buttonEndlessLoad->hide(); + } - if (infinite == "scroll") + if (infinite == "scroll") { connect(ui_scrollAreaResults, &VerticalScrollArea::endOfScrollReached, this, &SearchTab::endlessLoad); + } - if (infinite != "disabled" && ui_checkMergeResults != nullptr) + if (infinite != "disabled" && ui_checkMergeResults != nullptr) { connect(ui_checkMergeResults, &QCheckBox::toggled, this, &SearchTab::setMergeResultsMode); + } } SearchTab::~SearchTab() @@ -87,8 +82,9 @@ SearchTab::~SearchTab() qDeleteAll(m_checkboxes); m_checkboxes.clear(); - for (QLayout *layout : qAsConst(m_siteLayouts)) - { clearLayout(layout); } + for (QLayout *layout : qAsConst(m_siteLayouts)) { + clearLayout(layout); + } qDeleteAll(m_siteLayouts); m_siteLayouts.clear(); m_layouts.clear(); @@ -100,10 +96,10 @@ SearchTab::~SearchTab() void SearchTab::setSelectedSources(QSettings *settings) { QStringList sav = settings->value("sites").toStringList(); - for (const QString &key : sav) - { - if (!m_sites.contains(key)) + for (const QString &key : sav) { + if (!m_sites.contains(key)) { continue; + } m_selectedSources.append(m_sites.value(key)); } @@ -128,28 +124,21 @@ void SearchTab::setTagsFromPages(const QMap> // Tags for this page QList tagList; QStringList tagsGot; - for (const auto &ps : pages) - { + for (const auto &ps : pages) { QList tags = ps.last()->tags(); - for (const Tag &tag : tags) - { - if (!tag.text().isEmpty()) - { + for (const Tag &tag : tags) { + if (!tag.text().isEmpty()) { // Add to auto-complete list if it has enough count - if (tag.count() >= m_settings->value("tagsautoadd", 10).toInt() && !m_completion.contains(tag.text())) - { + if (tag.count() >= m_settings->value("tagsautoadd", 10).toInt() && !m_completion.contains(tag.text())) { m_profile->addAutoComplete(tag.text()); m_completion.append(tag.text()); } // If we already have this tag in the list, we increase its count - if (tagsGot.contains(tag.text())) - { + if (tagsGot.contains(tag.text())) { const int index = tagsGot.indexOf(tag.text()); tagList[index].setCount(tagList[index].count() + tag.count()); - } - else - { + } else { tagList.append(tag); tagsGot.append(tag.text()); } @@ -168,57 +157,72 @@ QStringList SearchTab::reasonsToFail(Page *page, const QStringList &completion, { QStringList reasons = QStringList(); + // Filtered images + if (page->pageImageCount() > 0) { + reasons.append(tr("all images filtered")); + return reasons; + } + // If the request yielded no source, the server may be offline - if (!page->hasSource()) - { reasons.append(tr("server offline")); } + if (!page->hasSource()) { + reasons.append(tr("server offline")); + } // Some sources do not allow more than two tags per search - if (page->search().count() > 2) - { reasons.append(tr("too many tags")); } + if (page->search().count() > 2) { + reasons.append(tr("too many tags")); + } // Many sources don't allow browsing after page 1000 - if (page->page() > 1000) - { reasons.append(tr("page too far")); } + if (page->page() > 1000) { + reasons.append(tr("page too far")); + } // Auto-correct - if (meant != nullptr && !page->search().isEmpty()) - { + if (meant != nullptr && !page->search().isEmpty()) { QMap results, clean; QList modifiers = QList() << '~' << '-'; int c = 0; - for (QString tag : page->search()) - { - if (modifiers.contains(tag[0])) + for (QString tag : page->search()) { + QChar modifier; + if (modifiers.contains(tag[0])) { + modifier = tag[0]; tag = tag.mid(1); + } int lev = qCeil((tag.length() - 1) / 4.0); - for (const QString &comp : completion) - { + for (const QString &comp : completion) { // Ignore tags that are too long - if (abs(comp.length() - tag.length()) > lev) + if (abs(comp.length() - tag.length()) > lev) { continue; + } const int d = levenshtein(tag, comp); - if (d < lev) - { - if (results[tag].isEmpty()) - { c++; } + if (d < lev) { + if (results[tag].isEmpty()) { + c++; + } results[tag] = "" + comp + ""; clean[tag] = comp; lev = d; } } - if (lev == 0) - { + if (lev == 0) { results[tag] = tag; c--; } + + if (!modifier.isNull() && results.contains(tag)) { + results[tag].prepend(modifier); + if (clean.contains(tag)) { + clean[tag].prepend(modifier); + } + } } - if (c > 0) - { + if (c > 0) { QStringList res = results.values(), cl = clean.values(); *meant = QString(R"(%2)").arg(cl.join(" ").toHtmlEscaped(), res.join(" ")); } @@ -240,10 +244,12 @@ void SearchTab::clear() m_parent->setWiki(QString(), this); // Clear layout - for (int i = 0; i < ui_layoutResults->rowCount(); ++i) - { ui_layoutResults->setRowMinimumHeight(i, 0); } - for (QLayout *layout : qAsConst(m_siteLayouts)) - { clearLayout(layout); } + for (int i = 0; i < ui_layoutResults->rowCount(); ++i) { + ui_layoutResults->setRowMinimumHeight(i, 0); + } + for (QLayout *layout : qAsConst(m_siteLayouts)) { + clearLayout(layout); + } qDeleteAll(m_siteLayouts); m_siteLayouts.clear(); m_layouts.clear(); @@ -253,19 +259,17 @@ void SearchTab::clear() clearLayout(ui_layoutResults); // Abort current loadings - for (const auto &pages : qAsConst(m_pages)) - { - for (const auto &page : pages) - { + for (const auto &pages : qAsConst(m_pages)) { + for (const auto &page : pages) { page->abort(); page->abortTags(); } } - for (auto it = m_thumbnailsLoading.constBegin(); it != m_thumbnailsLoading.constEnd(); ++it) - { + for (auto it = m_thumbnailsLoading.constBegin(); it != m_thumbnailsLoading.constEnd(); ++it) { QNetworkReply *reply = it.key(); - if (reply->isRunning()) - { reply->abort(); } + if (reply->isRunning()) { + reply->abort(); + } } m_pages.clear(); @@ -283,8 +287,7 @@ TextEdit *SearchTab::createAutocomplete() connect(ret, &TextEdit::addedFavorite, this, &SearchTab::setFavoriteImage); // Add auto-complete if necessary - if (m_settings->value("autocompletion", true).toBool()) - { + if (m_settings->value("autocompletion", true).toBool()) { auto *completer = new QCompleter(m_completion, ret); completer->setCaseSensitivity(Qt::CaseInsensitive); @@ -297,14 +300,11 @@ TextEdit *SearchTab::createAutocomplete() void SearchTab::setMergeResultsMode(bool merged) { // Restore endless loading mode - if (merged == m_pageMergedMode) - { + if (merged == m_pageMergedMode) { setEndlessLoadingMode(m_endlessLoadingEnabledPast); } - // Disable endless loading - else - { + else { m_endlessLoadingEnabledPast = m_endlessLoadingEnabled; setEndlessLoadingMode(false); } @@ -313,16 +313,18 @@ void SearchTab::setMergeResultsMode(bool merged) void SearchTab::setEndlessLoadingMode(bool enabled) { // Toggle endless loading button - if (ui_buttonEndlessLoad != nullptr && m_settings->value("infiniteScroll", "disabled") == "button") + if (ui_buttonEndlessLoad != nullptr && m_settings->value("infiniteScroll", "disabled") == "button") { ui_buttonEndlessLoad->setVisible(enabled); + } m_endlessLoadingEnabled = enabled; } void SearchTab::finishedLoading(Page *page) { - if (m_stop) + if (m_stop) { return; + } m_lastPage = page->page(); m_lastPageMinId = page->minId(); @@ -331,11 +333,13 @@ void SearchTab::finishedLoading(Page *page) // Filter images depending on tabs QList> validImages; QString error; - for (const QSharedPointer &img : page->images()) - if (validateImage(img, error)) + for (const QSharedPointer &img : page->images()) { + if (validateImage(img, error)) { validImages.append(img); - else if (!error.isEmpty()) + } else if (!error.isEmpty()) { log(error); + } + } m_validImages.insert(page, validImages); // Remove already existing images for merged results @@ -347,16 +351,18 @@ void SearchTab::finishedLoading(Page *page) updatePaginationButtons(page); addResultsPage(page, imgs, merged); - if (!m_settings->value("useregexfortags", true).toBool()) + if (!m_settings->value("useregexfortags", true).toBool()) { setTagsFromPages(m_pages); + } postLoading(page, imgs); } void SearchTab::failedLoading(Page *page) { - if (m_stop) + if (m_stop) { return; + } const bool merged = ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked(); addResultsPage(page, QList>(), merged); @@ -371,8 +377,7 @@ void SearchTab::httpsRedirect(Page *page) const QString action = settings->value("ssl_autocorrect", "ask").toString(); bool setSsl = action == "always"; - if (action == "ask") - { + if (action == "ask") { QMessageBox box(this); box.setWindowTitle(tr("HTTPS redirection detected")); box.setText(tr("An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'.").arg(page->site()->url())); @@ -382,21 +387,19 @@ void SearchTab::httpsRedirect(Page *page) QPushButton *never = box.addButton(tr("Never"), QMessageBox::NoRole); box.exec(); - if (box.clickedButton() == yes) - { setSsl = true; } - else if (box.clickedButton() == always) - { + if (box.clickedButton() == yes) { + setSsl = true; + } else if (box.clickedButton() == always) { setSsl = true; settings->setValue("ssl_autocorrect", "always"); + } else if (box.clickedButton() == neverWebsite) { + page->site()->setSetting("ssl_never_correct", true, false); + } else if (box.clickedButton() == never) { + settings->setValue("ssl_autocorrect", "never"); } - else if (box.clickedButton() == neverWebsite) - { page->site()->setSetting("ssl_never_correct", true, false); } - else if (box.clickedButton() == never) - { settings->setValue("ssl_autocorrect", "never"); } } - if (setSsl) - { + if (setSsl) { log(QStringLiteral("[%1] Enabling HTTPS").arg(page->site()->url()), Logger::Info); page->site()->setSetting("ssl", true, false); } @@ -409,19 +412,19 @@ void SearchTab::postLoading(Page *page, const QList> &imgs const bool merged = ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked(); const bool finished = m_page == m_pages.count() || (merged && ui_progressMergeResults != nullptr && ui_progressMergeResults->value() == ui_progressMergeResults->maximum()); - if (merged) - { + if (merged) { // Increase the progress bar status - if (ui_progressMergeResults != nullptr) + if (ui_progressMergeResults != nullptr) { ui_progressMergeResults->setValue(ui_progressMergeResults->value() + 1); + } // Hide progress bar when we load the last page - if (ui_stackedMergeResults != nullptr && finished) + if (ui_stackedMergeResults != nullptr && finished) { ui_stackedMergeResults->setCurrentIndex(1); + } // Create the label when loading the first page - if (m_page == 1 && m_siteLabels.isEmpty()) - { + if (m_page == 1 && m_siteLabels.isEmpty()) { QLabel *txt = new QLabel(this); txt->setOpenExternalLinks(true); setMergedLabelText(txt, m_images); @@ -429,32 +432,30 @@ void SearchTab::postLoading(Page *page, const QList> &imgs ui_layoutResults->addWidget(txt, 0, 0); ui_layoutResults->setRowMinimumHeight(0, txt->sizeHint().height() + 10); + } else { + setMergedLabelText(m_siteLabels[nullptr], m_images); } - else - { setMergedLabelText(m_siteLabels[nullptr], m_images); } } // Load thumbnails - for (const auto &img : imgs) - { + for (const auto &img : imgs) { const QUrl thumbnailUrl = img->url(Image::Size::Thumbnail); - if (thumbnailUrl.isValid()) - { loadImageThumbnail(page, img, thumbnailUrl); } + if (thumbnailUrl.isValid()) { + loadImageThumbnail(page, img, thumbnailUrl); + } } // Re-enable endless loading if all sources have reached the last page - if (finished) - { + if (finished) { bool allFinished = true; - for (auto ps : qAsConst(m_pages)) - { + for (auto ps : qAsConst(m_pages)) { const int pagesCount = ps.first()->pagesCount(); const int imagesPerPage = ps.first()->imagesPerPage(); - if (ps.last()->page() < pagesCount && ps.last()->pageImageCount() >= imagesPerPage) + if (ps.last()->page() < pagesCount && ps.last()->pageImageCount() >= imagesPerPage) { allFinished = false; + } } - if (!allFinished) - { + if (!allFinished) { setEndlessLoadingMode(true); } } @@ -466,20 +467,24 @@ void SearchTab::postLoading(Page *page, const QList> &imgs void SearchTab::updatePaginationButtons(Page *page) { + const int pageNum = ui_spinPage->value(); + // Update max page counter int pageCount = page->pagesCount(); int maxPages = page->maxPagesCount(); - if (pageCount <= 0 && maxPages > 0) + if (pageCount <= 0 && maxPages > 0) { pageCount = maxPages; - if (pageCount > m_pagemax || m_pagemax == -1) + } + if (pageCount > m_pagemax || m_pagemax == -1) { m_pagemax = pageCount; + } // Update page spinbox max value - ui_spinPage->setMaximum(page->imagesCount() == -1 || page->pagesCount() == -1 ? 100000 : m_pagemax); + ui_spinPage->setMaximum(page->imagesCount() == -1 || page->pagesCount() == -1 ? 100000 : qMax(1, qMax(pageNum, m_pagemax))); // Enable/disable buttons - ui_buttonNextPage->setEnabled(m_pagemax > ui_spinPage->value() || page->imagesCount() == -1 || page->pagesCount() == -1 || (page->imagesCount() == 0 && page->pageImageCount() > 0)); - ui_buttonLastPage->setEnabled(m_pagemax > ui_spinPage->value() || page->imagesCount() == -1 || page->pagesCount() == -1); + ui_buttonNextPage->setEnabled(m_pagemax > pageNum || page->imagesCount() == -1 || page->pagesCount() == -1 || (page->imagesCount() == 0 && page->pageImageCount() > 0)); + ui_buttonLastPage->setEnabled(m_pagemax > pageNum || page->imagesCount() == -1 || page->pagesCount() == -1); } void SearchTab::finishedLoadingTags(Page *page) @@ -487,8 +492,7 @@ void SearchTab::finishedLoadingTags(Page *page) setTagsFromPages(m_pages); // Wiki - if (!page->wiki().isEmpty()) - { + if (!page->wiki().isEmpty()) { m_wiki = page->wiki(); m_parent->setWiki(m_wiki, this); } @@ -498,14 +502,17 @@ void SearchTab::finishedLoadingTags(Page *page) // Update image and page count QList> imgs; QString error; - for (const QSharedPointer &img : page->images()) - if (validateImage(img, error)) + for (const QSharedPointer &img : page->images()) { + if (validateImage(img, error)) { imgs.append(img); + } + } - if (ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked() && m_siteLabels.contains(nullptr)) + if (ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked() && m_siteLabels.contains(nullptr)) { setMergedLabelText(m_siteLabels[nullptr], m_images); - else if (m_siteLabels.contains(page->site())) + } else if (m_siteLabels.contains(page->site())) { setPageLabelText(m_siteLabels[page->site()], page, imgs); + } } void SearchTab::loadImageThumbnail(Page *page, QSharedPointer img, const QUrl &url) @@ -521,52 +528,44 @@ void SearchTab::loadImageThumbnail(Page *page, QSharedPointer img, const void SearchTab::finishedLoadingPreview() { - if (m_stop) + if (m_stop) { return; + } auto *reply = qobject_cast(sender()); // Aborted - if (reply->error() == QNetworkReply::OperationCanceledError) - { + if (reply->error() == QNetworkReply::OperationCanceledError) { reply->deleteLater(); return; } // Try to find associated image QSharedPointer img; - if (m_thumbnailsLoading.contains(reply)) - { + if (m_thumbnailsLoading.contains(reply)) { img = m_thumbnailsLoading[reply]; m_thumbnailsLoading.remove(reply); - } - else - { + } else { log(QStringLiteral("Could not find image related to loaded thumbnail '%1'").arg(reply->url().toString()), Logger::Error); return; } // Check redirection QUrl redirection = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redirection.isEmpty()) - { + if (!redirection.isEmpty()) { loadImageThumbnail(img->page(), img, redirection); reply->deleteLater(); return; } // Loading error - if (reply->error() != QNetworkReply::NoError) - { + if (reply->error() != QNetworkReply::NoError) { const QString ext = getExtension(reply->url()); - if (ext != "jpg") - { + if (ext != "jpg") { log(QStringLiteral("Error loading thumbnail (%1), new try with extension JPG").arg(reply->errorString()), Logger::Warning); const QUrl newUrl = setExtension(reply->url(), "jpg"); loadImageThumbnail(img->page(), img, newUrl); - } - else - { + } else { log(QStringLiteral("Error loading thumbnail (%1)").arg(reply->errorString()), Logger::Error); } @@ -578,39 +577,38 @@ void SearchTab::finishedLoadingPreview() QPixmap preview; preview.loadFromData(reply->readAll()); reply->deleteLater(); - if (preview.isNull()) - { + if (preview.isNull()) { log(QStringLiteral("One of the thumbnails is empty (`%1`).").arg(img->url(Image::Size::Thumbnail).toString()), Logger::Error); - if (img->hasTag(QStringLiteral("flash"))) - { preview.load(QStringLiteral(":/images/flash.png")); } - else - { return; } + if (img->hasTag(QStringLiteral("flash"))) { + preview.load(QStringLiteral(":/images/flash.png")); + } else { + return; + } } img->setPreviewImage(preview); // Download whitelist images on thumbnail view Blacklist whitelistedTags; - for (const QString &tag : m_settings->value("whitelistedtags").toString().split(" ", QString::SkipEmptyParts)) - { whitelistedTags.add(tag); } + for (const QString &tag : m_settings->value("whitelistedtags").toString().split(" ", QString::SkipEmptyParts)) { + whitelistedTags.add(tag); + } QStringList detected = m_profile->getBlacklist().match(img->tokens(m_profile)); QStringList whitelisted = whitelistedTags.match(img->tokens(m_profile)); - if (!whitelisted.isEmpty() && m_settings->value("whitelist_download", "image").toString() == "page") - { + if (!whitelisted.isEmpty() && m_settings->value("whitelist_download", "image").toString() == "page") { bool download = false; - if (!detected.isEmpty()) - { + if (!detected.isEmpty()) { const int reponse = QMessageBox::question(this, "Grabber", tr("Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway?").arg(whitelisted.join(", "), detected.join(", ")), QMessageBox::Yes | QMessageBox::Open | QMessageBox::No); - if (reponse == QMessageBox::Yes) - { download = true; } - else if (reponse == QMessageBox::Open) - { openImage(img); } + if (reponse == QMessageBox::Yes) { + download = true; + } else if (reponse == QMessageBox::Open) { + openImage(img); + } + } else { + download = true; } - else - { download = true; } - if (download) - { - auto downloader = new ImageDownloader(m_profile, img, m_settings->value("Save/filename").toString(), m_settings->value("Save/path").toString(), 1, true, true, true, this); + if (download) { + auto downloader = new ImageDownloader(m_profile, img, m_settings->value("Save/filename").toString(), m_settings->value("Save/path").toString(), 1, true, true, this); downloader->save(); connect(downloader, &ImageDownloader::saved, downloader, &ImageDownloader::deleteLater); } @@ -625,14 +623,15 @@ void SearchTab::finishedLoadingPreview() */ double getImageKnownTagProportion(const QSharedPointer &img) { - if (img->tags().isEmpty()) + if (img->tags().isEmpty()) { return 0; + } int known = 0; - for (const Tag &tag : img->tags()) - { - if (!tag.type().isUnknown()) + for (const Tag &tag : img->tags()) { + if (!tag.type().isUnknown()) { known++; + } } return (static_cast(known) / static_cast(img->tags().count())); @@ -641,11 +640,11 @@ double getImageKnownTagProportion(const QSharedPointer &img) QList> SearchTab::mergeResults(int page, const QList> &results) { QMap pageMd5s; - for (const QSharedPointer &img : qAsConst(m_images)) - { + for (const QSharedPointer &img : qAsConst(m_images)) { QString md5 = img->md5(); - if (md5.isEmpty()) + if (md5.isEmpty()) { continue; + } const double proportion = getImageKnownTagProportion(img); pageMd5s[md5] = proportion; @@ -653,28 +652,23 @@ QList> SearchTab::mergeResults(int page, const QList imgMd5s; - for (int i = 0; i < m_images.count(); ++i) + for (int i = 0; i < m_images.count(); ++i) { imgMd5s.insert(m_images[i]->md5(), i); + } QList> ret; - for (const QSharedPointer &img : results) - { + for (const QSharedPointer &img : results) { QString md5 = img->md5(); const double proportion = getImageKnownTagProportion(img); - if (md5.isEmpty() || ((!pageMd5s.contains(md5) || proportion > pageMd5s[md5]) && !containsMergedMd5(page, md5))) - { - if (pageMd5s.contains(md5) && proportion > pageMd5s[md5]) - { + if (md5.isEmpty() || ((!pageMd5s.contains(md5) || proportion > pageMd5s[md5]) && !containsMergedMd5(page, md5))) { + if (pageMd5s.contains(md5) && proportion > pageMd5s[md5]) { m_images[imgMd5s[md5]] = img; pageMd5s[md5] = proportion; - } - else - { + } else { ret.append(img); - if (!md5.isEmpty()) - { + if (!md5.isEmpty()) { pageMd5s[md5] = proportion; addMergedMd5(page, md5); } @@ -687,10 +681,8 @@ QList> SearchTab::mergeResults(int page, const QList> &pair : m_mergedMd5s) - { - if (pair.first == page) - { + for (QPair> &pair : m_mergedMd5s) { + if (pair.first == page) { pair.second.insert(md5); return; } @@ -703,14 +695,15 @@ void SearchTab::addMergedMd5(int page, const QString &md5) bool SearchTab::containsMergedMd5(int page, const QString &md5) { - for (const QPair> &pair : qAsConst(m_mergedMd5s)) - { + for (const QPair> &pair : qAsConst(m_mergedMd5s)) { // We only check the sets before the page was loaded - if (pair.first == page) + if (pair.first == page) { break; + } - if (pair.second.contains(md5)) + if (pair.second.contains(md5)) { return true; + } } return false; @@ -718,19 +711,20 @@ bool SearchTab::containsMergedMd5(int page, const QString &md5) void SearchTab::addResultsPage(Page *page, const QList> &imgs, bool merged, const QString &noResultsMessage) { - if (merged) + if (merged) { return; + } const int pos = m_pages.keys().indexOf(page->website()); - if (pos < 0) + if (pos < 0) { return; + } const int page_x = pos % ui_spinColumns->value(); const int page_y = (pos / ui_spinColumns->value()) * 2; Site *site = page->site(); - if (!m_siteLabels.contains(site)) - { + if (!m_siteLabels.contains(site)) { QLabel *txt = new QLabel(this); txt->setOpenExternalLinks(true); m_siteLabels.insert(site, txt); @@ -740,8 +734,9 @@ void SearchTab::addResultsPage(Page *page, const QList> &i } setPageLabelText(m_siteLabels[site], page, imgs, noResultsMessage); - if (m_siteLayouts.contains(page->site()) && m_pages.value(page->website()).count() == 1) - { addLayout(m_siteLayouts[page->site()], page_y + 1, page_x); } + if (m_siteLayouts.contains(page->site()) && m_pages.value(page->website()).count() == 1) { + addLayout(m_siteLayouts[page->site()], page_y + 1, page_x); + } } void SearchTab::setMergedLabelText(QLabel *txt, const QList> &imgs) { @@ -750,33 +745,33 @@ void SearchTab::setMergedLabelText(QLabel *txt, const QListvalue() + m_endlessLoadOffset; int lastPage = ui_spinPage->value() + m_endlessLoadOffset; - for (const auto &ps : qAsConst(m_pages)) - { + for (const auto &ps : qAsConst(m_pages)) { const QSharedPointer first = ps.first(); const int imagesCount = first->imagesCount(); - if (imagesCount > 0) + if (imagesCount > 0) { sumImages += first->imagesCount(); + } - for (const QSharedPointer &p : ps) - { + for (const QSharedPointer &p : ps) { const int pagesCount = p->pagesCount(); - if (pagesCount > maxPage) + if (pagesCount > maxPage) { maxPage = pagesCount; + } - if (p->page() < firstPage) + if (p->page() < firstPage) { firstPage = p->page(); - if (p->page() > lastPage) + } + if (p->page() > lastPage) { lastPage = p->page(); + } } } QString links; - if (m_pages.count() > 5) - { links = "Multiple sources"; } - else - { - for (const auto &ps : qAsConst(m_pages)) - { + if (m_pages.count() > 5) { + links = "Multiple sources"; + } else { + for (const auto &ps : qAsConst(m_pages)) { const auto &p = ps.last(); links += QString(!links.isEmpty() ? ", " : QString()) + "url().toString().toHtmlEscaped() + "\">" + p->site()->name() + ""; } @@ -794,32 +789,30 @@ void SearchTab::setPageLabelText(QLabel *txt, Page *page, const QList 0 ? page->page() : 0; int lastPage = imgs.count() > 0 ? page->page() : 0; int totalCount = 0; - for (const QSharedPointer &p : m_pages[page->website()]) - { - if (p->images().count() == 0) + for (const QSharedPointer &p : m_pages[page->website()]) { + if (p->images().count() == 0) { continue; - if (p->page() < firstPage || firstPage == 0) + } + if (p->page() < firstPage || firstPage == 0) { firstPage = p->page(); - if (p->page() > lastPage) + } + if (p->page() > lastPage) { lastPage = p->page(); + } totalCount += p->images().count(); } // No results message - if (totalCount == 0) - { + if (imgs.isEmpty()) { QString meant; QStringList reasons = reasonsToFail(page, m_completion, &meant); - if (!meant.isEmpty() && ui_widgetMeant != nullptr) - { + if (!meant.isEmpty() && ui_widgetMeant != nullptr) { ui_widgetMeant->show(); ui_labelMeant->setText(meant); } const QString msg = noResultsMessage == nullptr ? tr("No result") : noResultsMessage; - txt->setText("url().toString().toHtmlEscaped()+"\">"+page->site()->name()+" - "+msg+(reasons.count() > 0 ? "
"+tr("Possible reasons: %1").arg(reasons.join(", ")) : QString())); - } - else - { + txt->setText("url().toString().toHtmlEscaped() + "\">" + page->site()->name() + " - " + msg + (reasons.count() > 0 ? "
" + tr("Possible reasons: %1").arg(reasons.join(", ")) : QString())); + } else { const QString pageLabel = firstPage != lastPage ? QString("%1-%2").arg(firstPage).arg(lastPage) : QString::number(lastPage); const QString pageCountStr = pageCount > 0 ? (page->pagesCount(false) == -1 ? "~" : QString()) + QString::number(pageCount) @@ -832,22 +825,21 @@ void SearchTab::setPageLabelText(QLabel *txt, Page *page, const QListsetText("url().toString().toHtmlEscaped() + "\">" + page->site()->name() + " - " + countLabel); } - /*if (page->search().join(" ") != m_search->toPlainText() && m_settings->value("showtagwarning", true).toBool()) - { + /*if (page->search().join(" ") != m_search->toPlainText() && m_settings->value("showtagwarning", true).toBool()) { QStringList uncommon = m_search->toPlainText().toLower().trimmed().split(" ", QString::SkipEmptyParts); uncommon.append(m_settings->value("add").toString().toLower().trimmed().split(" ", QString::SkipEmptyParts)); - for (int i = 0; i < page->search().size(); i++) - { - if (uncommon.contains(page->search().at(i))) - { uncommon.removeAll(page->search().at(i)); } + for (int i = 0; i < page->search().size(); i++) { + if (uncommon.contains(page->search().at(i))) { + uncommon.removeAll(page->search().at(i)); + } + } + if (!uncommon.isEmpty()) { + txt->setText(txt->text()+"
"+QString(tr("Des modificateurs ont été otés de la recherche car ils ne sont pas compatibles avec cet imageboard : %1.")).arg(uncommon.join(" "))); } - if (!uncommon.isEmpty()) - { txt->setText(txt->text()+"
"+QString(tr("Des modificateurs ont été otés de la recherche car ils ne sont pas compatibles avec cet imageboard : %1.")).arg(uncommon.join(" "))); } }*/ // Show warnings - if (!page->errors().isEmpty() && m_settings->value("showwarnings", true).toBool()) - { + if (!page->errors().isEmpty() && m_settings->value("showwarnings", true).toBool()) { txt->setText(txt->text() + "
" + page->errors().join("
")); } } @@ -867,21 +859,22 @@ QBouton *SearchTab::createImageThumbnail(int position, const QSharedPointersetChecked(m_selectedImages.contains(img->url())); l->setInvertToggle(m_settings->value("invertToggle", false).toBool()); l->setToolTip(img->tooltip()); - if (img->previewImage().isNull()) - { l->scale(QPixmap(":/images/noimage.png"), upscale); } - else - { l->scale(img->previewImage(), upscale); } + if (img->previewImage().isNull()) { + l->scale(QPixmap(":/images/noimage.png"), upscale); + } else { + l->scale(img->previewImage(), upscale); + } l->setFlat(true); QString counter = img->counter(); - if (!counter.isEmpty()) - { l->setCounter(counter); } + if (!counter.isEmpty()) { + l->setCounter(counter); + } l->setContextMenuPolicy(Qt::CustomContextMenu); connect(l, &QWidget::customContextMenuRequested, this, [this, position, img]{ thumbnailContextMenu(position, img); }); - if (fixedWidthLayout) - { + if (fixedWidthLayout) { const int dim = qFloor(FIXED_IMAGE_WIDTH * upscale + borderSize * 2); l->setFixedSize(dim, dim); } @@ -898,13 +891,12 @@ QString getImageAlreadyExists(Image *img, Profile *profile) const QString path = settings->value("Save/path").toString().replace("\\", "/"); const QString fn = settings->value("Save/filename").toString(); - if (Filename(fn).needExactTags(img->parentSite()) == 0) - { - QStringList files = img->path(fn, path, 0, true, true, true, true); - for (const QString &file : files) - { - if (QFile(file).exists()) + if (Filename(fn).needExactTags(img->parentSite()) == 0) { + QStringList files = img->paths(fn, path, 0); + for (const QString &file : files) { + if (QFile(file).exists()) { return file; + } } } @@ -920,10 +912,11 @@ void SearchTab::thumbnailContextMenu(int position, const QSharedPointer & auto *mapperSave = new QSignalMapper(this); connect(mapperSave, SIGNAL(mapped(int)), this, SLOT(contextSaveImage(int))); QAction *actionSave; - if (!getImageAlreadyExists(img.data(), m_profile).isEmpty()) - { actionSave = new QAction(QIcon(":/images/status/error.png"), tr("Delete"), menu); } - else - { actionSave = new QAction(QIcon(":/images/icons/save.png"), tr("Save"), menu); } + if (!getImageAlreadyExists(img.data(), m_profile).isEmpty()) { + actionSave = new QAction(QIcon(":/images/status/error.png"), tr("Delete"), menu); + } else { + actionSave = new QAction(QIcon(":/images/icons/save.png"), tr("Save"), menu); + } menu->insertAction(first, actionSave); connect(actionSave, SIGNAL(triggered()), mapperSave, SLOT(map())); mapperSave->setMapping(actionSave, position); @@ -936,8 +929,7 @@ void SearchTab::thumbnailContextMenu(int position, const QSharedPointer & connect(actionSaveAs, SIGNAL(triggered()), mapperSaveAs, SLOT(map())); mapperSaveAs->setMapping(actionSaveAs, position); - if (!m_selectedImagesPtrs.empty()) - { + if (!m_selectedImagesPtrs.empty()) { QAction *actionSaveSelected = new QAction(QIcon(":/images/icons/save.png"), tr("Save selected"), menu); connect(actionSaveSelected, &QAction::triggered, this, &SearchTab::contextSaveSelected); menu->insertAction(first, actionSaveSelected); @@ -953,14 +945,13 @@ void SearchTab::contextSaveImage(int position) Image *img = image.data(); QString already = getImageAlreadyExists(img, m_profile); - if (!already.isEmpty()) - { QFile(already).remove(); } - else - { + if (!already.isEmpty()) { + QFile(already).remove(); + } else { const QString fn = m_settings->value("Save/filename").toString(); const QString path = m_settings->value("Save/path").toString(); - auto downloader = new ImageDownloader(m_profile, image, fn, path, 1, true, true, true, this); + auto downloader = new ImageDownloader(m_profile, image, fn, path, 1, true, true, this); connect(downloader, &ImageDownloader::downloadProgress, this, &SearchTab::contextSaveImageProgress); connect(downloader, &ImageDownloader::saved, downloader, &ImageDownloader::deleteLater); downloader->save(); @@ -975,75 +966,91 @@ void SearchTab::contextSaveImageAs(int position) QString tmpPath; // If the MD5 is required for the filename, we first download the image - if (fn.contains("%md5") && image->md5().isEmpty()) - { + if (fn.contains("%md5") && image->md5().isEmpty()) { tmpPath = QDir::temp().absoluteFilePath("grabber-saveAs-" + QString::number(qrand(), 16)); QEventLoop loop; - ImageDownloader downloader(m_profile, image, QStringList() << tmpPath, 1, true, true, true, this); + ImageDownloader downloader(m_profile, image, QStringList() << tmpPath, 1, true, true, this); connect(&downloader, &ImageDownloader::saved, &loop, &QEventLoop::quit); downloader.save(); loop.exec(); } Filename format(fn); - QStringList filenames = format.path(*img, m_profile); + const QStringList filenames = format.path(*img, m_profile); const QString filename = filenames.first().section(QDir::separator(), -1); const QString lastDir = m_settings->value("Zoom/lastDir").toString(); QString path = QFileDialog::getSaveFileName(this, tr("Save image"), QDir::toNativeSeparators(lastDir + "/" + filename), "Images (*.png *.gif *.jpg *.jpeg)"); - if (!path.isEmpty()) - { + if (!path.isEmpty()) { path = QDir::toNativeSeparators(path); m_settings->setValue("Zoom/lastDir", path.section(QDir::separator(), 0, -2)); - if (!tmpPath.isEmpty()) - { QFile::rename(tmpPath, path); } - else - { - auto downloader = new ImageDownloader(m_profile, image, QStringList() << path, 1, true, true, true, this); + if (!tmpPath.isEmpty()) { + QFile::rename(tmpPath, path); + } else { + auto downloader = new ImageDownloader(m_profile, image, QStringList() << path, 1, true, true, this); connect(downloader, &ImageDownloader::saved, downloader, &ImageDownloader::deleteLater); downloader->save(); } + } else if (!tmpPath.isEmpty()) { + QFile::remove(tmpPath); } - else if (!tmpPath.isEmpty()) - { QFile::remove(tmpPath); } } void SearchTab::contextSaveSelected() { const QString fn = m_settings->value("Save/filename").toString(); const QString path = m_settings->value("Save/path").toString(); - for (const QSharedPointer &img : qAsConst(m_selectedImagesPtrs)) - { - auto downloader = new ImageDownloader(m_profile, img, fn, path, 1, true, true, true, this); + for (const QSharedPointer &img : qAsConst(m_selectedImagesPtrs)) { + auto downloader = new ImageDownloader(m_profile, img, fn, path, 1, true, true, this); connect(downloader, &ImageDownloader::downloadProgress, this, &SearchTab::contextSaveImageProgress); connect(downloader, &ImageDownloader::saved, downloader, &ImageDownloader::deleteLater); downloader->save(); } } -void SearchTab::contextSaveImageProgress(QSharedPointer img, qint64 v1, qint64 v2) +void SearchTab::contextSaveImageProgress(const QSharedPointer &img, qint64 v1, qint64 v2) { - if (m_boutons.contains(img.data())) - { m_boutons[img.data()]->setProgress(v1, v2); } + if (m_boutons.contains(img.data())) { + m_boutons[img.data()]->setProgress(v1, v2); + } +} + +QList> SearchTab::getPagesToDownload() +{ + const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); + + QList> pages; + if (unloaded) { + QStringList keys = m_sites.keys(); + for (int i = 0; i < m_checkboxes.count(); i++) { + if (m_checkboxes[i]->isChecked() && m_pages.contains(keys[i])) { + pages.append(m_pages[keys[i]].first()); + } + } + } else { + for (auto it = m_pages.begin(); it != m_pages.end(); ++it) { + pages.append(it.value().first()); + } + } + + return pages; } void SearchTab::addResultsImage(const QSharedPointer &img, Page *page, bool merge) { // Early return if the layout has already been removed Page *layoutKey = merge && m_layouts.contains(nullptr) ? nullptr : page; - if (!m_layouts.contains(layoutKey)) + if (!m_layouts.contains(layoutKey)) { return; + } // Calculate image absolute position int absolutePosition = m_images.indexOf(img); - if (absolutePosition < 0 && !img->md5().isEmpty()) - { + if (absolutePosition < 0 && !img->md5().isEmpty()) { int j = 0; - for (const QSharedPointer &i : page->images()) - { - if (i->md5() == img->md5()) - { + for (const QSharedPointer &i : page->images()) { + if (i->md5() == img->md5()) { absolutePosition = j; break; } @@ -1063,17 +1070,20 @@ void SearchTab::addResultsImage(const QSharedPointer &img, Page *page, bo layout->insertWidget(relativePosition, button); } -void SearchTab::addHistory(const QString &tags, int page, int ipp, int cols) +void SearchTab::addHistory(const SearchQuery &query, int page, int ipp, int cols) { QMap srch; - srch["tags"] = tags; + if (!query.gallery.isNull()) { + srch["gallery"] = query.gallery->name(); + } else { + srch["tags"] = query.tags.join(' '); + } srch["page"] = QString::number(page); srch["ipp"] = QString::number(ipp); srch["columns"] = QString::number(cols); m_history.append(srch); - if (m_history.size() > 1) - { + if (m_history.size() > 1) { m_history_cursor++; ui_buttonHistoryBack->setEnabled(true); ui_buttonHistoryNext->setEnabled(false); @@ -1081,8 +1091,9 @@ void SearchTab::addHistory(const QString &tags, int page, int ipp, int cols) } void SearchTab::historyBack() { - if (m_history_cursor <= 0) + if (m_history_cursor <= 0) { return; + } m_from_history = true; m_history_cursor--; @@ -1093,13 +1104,15 @@ void SearchTab::historyBack() setTags(m_history[m_history_cursor]["tags"]); ui_buttonHistoryNext->setEnabled(true); - if (m_history_cursor == 0) - { ui_buttonHistoryBack->setEnabled(false); } + if (m_history_cursor == 0) { + ui_buttonHistoryBack->setEnabled(false); + } } void SearchTab::historyNext() { - if (m_history_cursor >= m_history.size() - 1) + if (m_history_cursor >= m_history.size() - 1) { return; + } m_from_history = true; m_history_cursor++; @@ -1110,30 +1123,33 @@ void SearchTab::historyNext() setTags(m_history[m_history_cursor]["tags"]); ui_buttonHistoryBack->setEnabled(true); - if (m_history_cursor == m_history.size() - 1) - { ui_buttonHistoryNext->setEnabled(false); } + if (m_history_cursor == m_history.size() - 1) { + ui_buttonHistoryNext->setEnabled(false); + } } void SearchTab::getSel() { - if (m_selectedImagesPtrs.empty()) + if (m_selectedImagesPtrs.empty()) { return; + } - for (const QSharedPointer &img : qAsConst(m_selectedImagesPtrs)) - { - emit batchAddUnique(DownloadQueryImage(m_settings, *img, img->parentSite())); + for (const QSharedPointer &img : qAsConst(m_selectedImagesPtrs)) { + emit batchAddUnique(DownloadQueryImage(m_settings, img, img->parentSite())); } m_selectedImagesPtrs.clear(); m_selectedImages.clear(); - for (QBouton *l : qAsConst(m_boutons)) - { l->setChecked(false); } + for (QBouton *l : qAsConst(m_boutons)) { + l->setChecked(false); + } } void SearchTab::updateCheckboxes() { - if (ui_layoutSourcesList == nullptr) + if (ui_layoutSourcesList == nullptr) { return; + } log(QStringLiteral("Updating checkboxes.")); @@ -1143,21 +1159,21 @@ void SearchTab::updateCheckboxes() const int n = m_settings->value("Sources/Letters", 3).toInt(); int m = n; - for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) - { + for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) { Site *site = it.value(); QString url = site->url(); - if (url.startsWith("www.")) - { url = url.right(url.length() - 4); } - else if (url.startsWith("chan.")) - { url = url.right(url.length() - 5); } + if (url.startsWith("www.")) { + url = url.right(url.length() - 4); + } else if (url.startsWith("chan.")) { + url = url.right(url.length() - 5); + } - if (n < 0) - { + if (n < 0) { m = url.indexOf('.'); - if (n < -1 && url.indexOf('.', m + 1) != -1) - { m = url.indexOf('.', m + 1); } + if (n < -1 && url.indexOf('.', m + 1) != -1) { + m = url.indexOf('.', m + 1); + } } QCheckBox *c = new QCheckBox(url.left(m), this); @@ -1174,17 +1190,18 @@ void SearchTab::updateCheckboxes() void SearchTab::webZoom(int id) { - if (id < 0 || id >= m_images.count()) + if (id < 0 || id >= m_images.count()) { return; + } const QSharedPointer &image = m_images.at(id); QStringList detected = m_profile->getBlacklist().match(image->tokens(m_profile)); - if (!detected.isEmpty()) - { + if (!detected.isEmpty()) { const int reply = QMessageBox::question(parentWidget(), tr("Blacklist"), tr("%n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway?", "", detected.size()).arg(detected.join(", ")), QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::No) - { return; } + if (reply == QMessageBox::No) { + return; + } } openImage(image); @@ -1192,20 +1209,18 @@ void SearchTab::webZoom(int id) void SearchTab::openImage(const QSharedPointer &image) { - if (image->isGallery()) - { - m_parent->addGalleryTab(image->parentSite(), image->name(), image->md5()); + if (image->isGallery()) { + m_parent->addGalleryTab(image->parentSite(), image); return; } - if (m_settings->value("Zoom/singleWindow", false).toBool() && !m_lastZoomWindow.isNull()) - { + if (m_settings->value("Zoom/singleWindow", false).toBool() && !m_lastZoomWindow.isNull()) { m_lastZoomWindow->reuse(m_images, image, image->page()->site()); m_lastZoomWindow->activateWindow(); return; } - ZoomWindow *zoom = new ZoomWindow(m_images, image, image->page()->site(), m_profile, m_parent); + ZoomWindow *zoom = new ZoomWindow(m_images, image, image->page()->site(), m_profile, m_parent, this); connect(zoom, SIGNAL(linkClicked(QString)), this, SLOT(setTags(QString))); connect(zoom, SIGNAL(poolClicked(int, QString)), m_parent, SLOT(addPoolTab(int, QString))); zoom->show(); @@ -1214,19 +1229,19 @@ void SearchTab::openImage(const QSharedPointer &image) } -void SearchTab::mouseReleaseEvent(QMouseEvent *e) +void SearchTab::mousePressEvent(QMouseEvent *e) { - if (e->button() == Qt::XButton1) - { previousPage(); } - else if (e->button() == Qt::XButton2) - { nextPage(); } + if (e->button() == Qt::XButton1) { + previousPage(); + } else if (e->button() == Qt::XButton2) { + nextPage(); + } } void SearchTab::selectImage(const QSharedPointer &img) { - if (!m_selectedImagesPtrs.contains(img)) - { + if (!m_selectedImagesPtrs.contains(img)) { m_selectedImagesPtrs.append(img); m_selectedImages.append(img->url()); } @@ -1234,8 +1249,7 @@ void SearchTab::selectImage(const QSharedPointer &img) void SearchTab::unselectImage(const QSharedPointer &img) { - if (m_selectedImagesPtrs.contains(img)) - { + if (m_selectedImagesPtrs.contains(img)) { const int pos = m_selectedImagesPtrs.indexOf(img); m_selectedImagesPtrs.removeAt(pos); m_selectedImages.removeAt(pos); @@ -1247,14 +1261,11 @@ void SearchTab::toggleImage(const QSharedPointer &img) const bool selected = m_selectedImagesPtrs.contains(img); m_boutons[img.data()]->setChecked(!selected); - if (selected) - { + if (selected) { const int pos = m_selectedImagesPtrs.indexOf(img); m_selectedImagesPtrs.removeAt(pos); m_selectedImages.removeAt(pos); - } - else - { + } else { m_selectedImagesPtrs.append(img); m_selectedImages.append(img->url()); } @@ -1262,19 +1273,22 @@ void SearchTab::toggleImage(const QSharedPointer &img) void SearchTab::toggleImage(int id, bool toggle, bool range) { - if (toggle) + if (toggle) { selectImage(m_images[id]); - else + } else { unselectImage(m_images[id]); + } - if (range) - { - if (id > m_lastToggle) - for (int i = m_lastToggle + 1; i < id; ++i) + if (range) { + if (id > m_lastToggle) { + for (int i = m_lastToggle + 1; i < id; ++i) { toggleImage(m_images[i]); - else - for (int i = m_lastToggle - 1; i > id; --i) + } + } else { + for (int i = m_lastToggle - 1; i > id; --i) { toggleImage(m_images[i]); + } + } } m_lastToggle = id; @@ -1293,28 +1307,38 @@ void SearchTab::saveSources(const QList &sel, bool canLoad) { log(QStringLiteral("Saving sources...")); + // Reset page counter when adding a new source + for (Site *site : sel) { + if (!m_selectedSources.contains(site)) { + ui_spinPage->setValue(1); + } + } + QStringList sav; sav.reserve(sel.count()); - for (Site *enabled : sel) - { sav.append(enabled->url()); } + for (Site *enabled : sel) { + sav.append(enabled->url()); + } m_settings->setValue("sites", sav); m_selectedSources = sel; // Log into new sources - for (Site *site : sel) - { site->login(); } + for (Site *site : sel) { + site->login(); + } updateCheckboxes(); DONE(); m_mergedMd5s.clear(); - if (m_history.isEmpty() && canLoad) - { load(); } + if (m_history.isEmpty() && canLoad) { + load(); + } } -void SearchTab::loadTags(QStringList tags) +void SearchTab::loadTags(SearchQuery query) { log(QStringLiteral("Loading results...")); @@ -1323,15 +1347,17 @@ void SearchTab::loadTags(QStringList tags) ui_scrollAreaResults->setScrollEnabled(resultsScrollArea); // Append "additional tags" setting - tags.append(m_settings->value("add").toString().trimmed().split(" ", QString::SkipEmptyParts)); + if (query.gallery.isNull()) { + query.tags.append(m_settings->value("add").toString().trimmed().split(" ", QString::SkipEmptyParts)); + } // Save previous pages m_lastPages.clear(); - for (Site *sel : qAsConst(m_selectedSources)) - { + for (Site *sel : qAsConst(m_selectedSources)) { const QString &site = sel->url(); - if (m_pages.contains(site)) + if (m_pages.contains(site)) { m_lastPages.insert(site, m_pages[site].last()); + } } clear(); @@ -1345,28 +1371,31 @@ void SearchTab::loadTags(QStringList tags) ui_buttonNextPage->setEnabled(false); ui_buttonLastPage->setEnabled(false); - // Get the search values - const QString search = tags.join(" "); - - if (!m_from_history) - { addHistory(search, ui_spinPage->value(), ui_spinImagesPerPage->value(), ui_spinColumns->value()); } + if (!m_from_history) { + addHistory(query, ui_spinPage->value(), ui_spinImagesPerPage->value(), ui_spinColumns->value()); + } m_from_history = false; - if (search != m_lastTags && !m_lastTags.isNull()) - { m_mergedMd5s.clear(); } - if (search != m_lastTags && !m_lastTags.isNull() && m_history_cursor == m_history.size() - 1) - { ui_spinPage->setValue(1); } - m_lastTags = search; + if (m_hasLastQuery && query != m_lastQuery) { + m_mergedMd5s.clear(); + } + if (m_hasLastQuery && query != m_lastQuery && m_history_cursor == m_history.size() - 1) { + ui_spinPage->setValue(1); + } + m_lastQuery = query; + m_hasLastQuery = true; - if (ui_widgetMeant != nullptr) + if (ui_widgetMeant != nullptr) { ui_widgetMeant->hide(); + } ui_buttonFirstPage->setEnabled(ui_spinPage->value() > 1); ui_buttonPreviousPage->setEnabled(ui_spinPage->value() > 1); const bool merged = ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked(); m_pageMergedMode = merged; - if (merged) + if (merged) { m_layouts.insert(nullptr, createImagesLayout(m_settings)); + } loadPage(); @@ -1375,15 +1404,17 @@ void SearchTab::loadTags(QStringList tags) void SearchTab::endlessLoad() { - if (!m_endlessLoadingEnabled) + if (!m_endlessLoadingEnabled) { return; + } const bool rememberPage = m_settings->value("infiniteScrollRememberPage", false).toBool(); - if (rememberPage) + if (rememberPage) { ui_spinPage->setValue(ui_spinPage->value() + 1); - else + } else { m_endlessLoadOffset++; + } loadPage(); } @@ -1393,38 +1424,44 @@ void SearchTab::loadPage() const bool merged = ui_checkMergeResults != nullptr && ui_checkMergeResults->isChecked(); const int perpage = ui_spinImagesPerPage->value(); const int currentPage = ui_spinPage->value() + m_endlessLoadOffset; - const QStringList tags = m_lastTags.split(' '); setEndlessLoadingMode(false); - for (Site *site : loadSites()) - { + for (Site *site : loadSites()) { + // Stored URL + SearchQuery query = m_lastQuery; + if (m_lastUrls.contains(site->url())) { + query.url = m_lastUrls.take(site->url()); + } + // Load results - Page *page = new Page(m_profile, site, m_sites.values(), tags, currentPage, perpage, m_postFiltering->toPlainText().split(" ", QString::SkipEmptyParts), false, this, 0, m_lastPage, m_lastPageMinId, m_lastPageMaxId); + const QStringList postFiltering = (m_postFiltering->toPlainText() + " " + m_settings->value("globalPostFilter").toString()).split(' ', QString::SkipEmptyParts); + Page *page = new Page(m_profile, site, m_sites.values(), query, currentPage, perpage, postFiltering, false, this, 0, m_lastPage, m_lastPageMinId, m_lastPageMaxId); connect(page, &Page::finishedLoading, this, &SearchTab::finishedLoading); connect(page, &Page::failedLoading, this, &SearchTab::failedLoading); connect(page, &Page::httpsRedirect, this, &SearchTab::httpsRedirect); // Keep pointer to the new page - if (m_lastPages.contains(page->website())) - { page->setLastPage(m_lastPages[page->website()].data()); } - if (!m_pages.contains(page->website())) - { m_pages.insert(page->website(), QList>()); } + if (m_lastPages.contains(page->website())) { + page->setLastPage(m_lastPages[page->website()].data()); + } + if (!m_pages.contains(page->website())) { + m_pages.insert(page->website(), QList>()); + } m_pages[page->website()].append(QSharedPointer(page)); // Setup the layout - if (!merged) - { + if (!merged) { FixedSizeGridLayout *pageLayout = createImagesLayout(m_settings); m_layouts.insert(page, pageLayout); - if (!m_siteLayouts.contains(site)) - { m_siteLayouts.insert(site, new QVBoxLayout()); } + if (!m_siteLayouts.contains(site)) { + m_siteLayouts.insert(site, new QVBoxLayout()); + } m_siteLayouts[site]->addLayout(pageLayout); } // Load tags if necessary m_stop = false; - if (m_settings->value("useregexfortags", true).toBool()) - { + if (m_settings->value("useregexfortags", true).toBool()) { connect(page, &Page::finishedLoadingTags, this, &SearchTab::finishedLoadingTags); page->loadTags(); } @@ -1432,17 +1469,18 @@ void SearchTab::loadPage() // Start loading page->load(); } - if (merged && !m_layouts.empty() && m_endlessLoadOffset == 0) - { addLayout(m_layouts[nullptr], 1, 0); } + if (merged && !m_layouts.empty() && m_endlessLoadOffset == 0) { + addLayout(m_layouts[nullptr], 1, 0); + } m_page = 0; - if (merged && ui_progressMergeResults != nullptr) - { + if (merged && ui_progressMergeResults != nullptr) { ui_progressMergeResults->setValue(0); ui_progressMergeResults->setMaximum(m_pages.count()); } - if (ui_stackedMergeResults != nullptr) - { ui_stackedMergeResults->setCurrentIndex(merged ? 0 : 1); } + if (ui_stackedMergeResults != nullptr) { + ui_stackedMergeResults->setCurrentIndex(merged ? 0 : 1); + } } void SearchTab::addLayout(QLayout *layout, int row, int column) @@ -1459,8 +1497,7 @@ FixedSizeGridLayout *SearchTab::createImagesLayout(QSettings *settings) auto *l = new FixedSizeGridLayout(hSpace, vSpace); const bool fixedWidthLayout = settings->value("resultsFixedWidthLayout", false).toBool(); - if (fixedWidthLayout) - { + if (fixedWidthLayout) { const int borderSize = settings->value("borders", 3).toInt(); const qreal upscale = settings->value("thumbnailUpscale", 1.0).toDouble(); l->setFixedWidth(qFloor(FIXED_IMAGE_WIDTH * upscale + borderSize * 2)); @@ -1473,8 +1510,7 @@ FixedSizeGridLayout *SearchTab::createImagesLayout(QSettings *settings) bool SearchTab::validateImage(const QSharedPointer &img, QString &error) { QStringList detected = m_profile->getBlacklist().match(img->tokens(m_profile)); - if (!detected.isEmpty() && m_settings->value("hideblacklisted", false).toBool()) - { + if (!detected.isEmpty() && m_settings->value("hideblacklisted", false).toBool()) { error = QStringLiteral("Image #%1 ignored. Reason: %2.").arg(img->id()).arg("\"" + detected.join(", ") + "\""); return false; } @@ -1487,21 +1523,23 @@ QList SearchTab::loadSites() const void SearchTab::setSources(const QList &sources) -{ m_selectedSources = sources; } +{ + m_selectedSources = sources; + updateCheckboxes(); +} void SearchTab::toggleSource(const QString &url) { Site *site = m_sites.value(url); const int removed = m_selectedSources.removeAll(site); - if (removed == 0) + if (removed == 0) { m_selectedSources.append(site); + } } void SearchTab::setFavoriteImage(const QString &name) { - for (Favorite &fav : m_favorites) - { - if (fav.getName() == name) - { + for (Favorite &fav : m_favorites) { + if (fav.getName() == name) { fav.setImage(m_images.first()->previewImage()); m_profile->emitFavorite(); } @@ -1517,7 +1555,7 @@ const QString &SearchTab::wiki() const { return m_wiki; } void SearchTab::onLoad() -{ } +{} void SearchTab::firstPage() @@ -1527,16 +1565,14 @@ void SearchTab::firstPage() } void SearchTab::previousPage() { - if (ui_spinPage->value() > 1) - { + if (ui_spinPage->value() > 1) { ui_spinPage->setValue(ui_spinPage->value() - 1); load(); } } void SearchTab::nextPage() { - if (ui_spinPage->value() < ui_spinPage->maximum()) - { + if (ui_spinPage->value() < ui_spinPage->maximum()) { ui_spinPage->setValue(ui_spinPage->value() + 1); load(); } diff --git a/gui/src/tabs/search-tab.h b/gui/src/tabs/search-tab.h index 356e082ca..ef7043582 100644 --- a/gui/src/tabs/search-tab.h +++ b/gui/src/tabs/search-tab.h @@ -13,6 +13,7 @@ #include #include #include "models/image.h" +#include "models/search-query/search-query.h" class DownloadQueryGroup; @@ -30,11 +31,13 @@ class SearchTab : public QWidget { Q_OBJECT - public: + protected: SearchTab(Profile *profile, MainWindow *parent); + + public: ~SearchTab() override; void init(); - void mouseReleaseEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; virtual QList sources(); virtual QString tags() const = 0; const QList &results() const; @@ -55,7 +58,7 @@ class SearchTab : public QWidget protected: void setSelectedSources(QSettings *settings); void setTagsFromPages(const QMap>> &pages); - void addHistory(const QString &tags, int page, int ipp, int cols); + void addHistory(const SearchQuery &query, int page, int ipp, int cols); QStringList reasonsToFail(Page *page, const QStringList &completion = QStringList(), QString *meant = nullptr); void clear(); TextEdit *createAutocomplete(); @@ -63,12 +66,13 @@ class SearchTab : public QWidget QBouton *createImageThumbnail(int position, const QSharedPointer &img); FixedSizeGridLayout *createImagesLayout(QSettings *settings); void thumbnailContextMenu(int position, const QSharedPointer &img); + QList> getPagesToDownload(); protected slots: void contextSaveImage(int position); void contextSaveImageAs(int position); void contextSaveSelected(); - void contextSaveImageProgress(QSharedPointer img, qint64 v1, qint64 v2); + void contextSaveImageProgress(const QSharedPointer &img, qint64 v1, qint64 v2); void setMergeResultsMode(bool merged); void setEndlessLoadingMode(bool enabled); void toggleSource(const QString &url); @@ -100,7 +104,7 @@ class SearchTab : public QWidget // Results virtual void load() = 0; virtual void updateTitle() = 0; - void loadTags(QStringList tags); + void loadTags(SearchQuery query); void endlessLoad(); void loadPage(); virtual void addResultsPage(Page *page, const QList> &imgs, bool merged, const QString &noResultsMessage = nullptr); @@ -154,6 +158,7 @@ class SearchTab : public QWidget QSettings *m_settings; QString m_wiki; QMap>> m_validImages; + QMap m_lastUrls; QStringList m_completion; QMap> m_thumbnailsLoading; @@ -176,7 +181,8 @@ class SearchTab : public QWidget bool m_from_history; int m_history_cursor; QList> m_history; - QString m_lastTags; + SearchQuery m_lastQuery; + bool m_hasLastQuery = false; QList>> m_mergedMd5s; // UI stuff diff --git a/gui/src/tabs/tabs-loader.cpp b/gui/src/tabs/tabs-loader.cpp index 3067b1d4e..bae6c49cf 100644 --- a/gui/src/tabs/tabs-loader.cpp +++ b/gui/src/tabs/tabs-loader.cpp @@ -20,8 +20,7 @@ bool TabsLoader::load(const QString &path, QList &allTabs, int &curr const bool preload = settings->value("preloadAllTabs", false).toBool(); QFile f(path); - if (!f.open(QFile::ReadOnly)) - { + if (!f.open(QFile::ReadOnly)) { return false; } @@ -30,19 +29,15 @@ bool TabsLoader::load(const QString &path, QList &allTabs, int &curr f.reset(); // Version 1 is plain text - if (!header.startsWith("{")) - { + if (!header.startsWith("{")) { QString links = f.readAll().trimmed(); f.close(); QStringList tabs = links.split("\r\n"); - for (int j = 0; j < tabs.size(); j++) - { + for (int j = 0; j < tabs.size(); j++) { QStringList infos = tabs[j].split("¤"); - if (infos.size() > 3) - { - if (infos[infos.size() - 1] == "pool") - { + if (infos.size() > 3) { + if (infos[infos.size() - 1] == "pool") { auto *tab = new PoolTab(profile, parent); tab->ui->spinPool->setValue(infos[0].toInt()); tab->ui->comboSites->setCurrentIndex(infos[1].toInt()); @@ -52,9 +47,7 @@ bool TabsLoader::load(const QString &path, QList &allTabs, int &curr tab->setTags(infos[2], preload); allTabs.append(tab); - } - else - { + } else { auto *tab = new TagTab(profile, parent); tab->ui->spinPage->setValue(infos[1].toInt()); tab->ui->spinImagesPerPage->setValue(infos[2].toInt()); @@ -82,12 +75,12 @@ bool TabsLoader::load(const QString &path, QList &allTabs, int &curr { currentTab = object["current"].toInt(); QJsonArray tabs = object["tabs"].toArray(); - for (auto tabJson : tabs) - { + for (auto tabJson : tabs) { QJsonObject infos = tabJson.toObject(); SearchTab *tab = loadTab(infos, profile, parent, preload); - if (tab != nullptr) + if (tab != nullptr) { allTabs.append(tab); + } } return true; } @@ -102,23 +95,21 @@ SearchTab *TabsLoader::loadTab(QJsonObject info, Profile *profile, MainWindow *p { QString type = info["type"].toString(); - if (type == "tag") - { + if (type == "tag") { auto *tab = new TagTab(profile, parent); - if (tab->read(info, preload)) + if (tab->read(info, preload)) { return tab; - } - else if (type == "pool") - { + } + } else if (type == "pool") { auto *tab = new PoolTab(profile, parent); - if (tab->read(info, preload)) + if (tab->read(info, preload)) { return tab; - } - else if (type == "gallery") - { + } + } else if (type == "gallery") { auto *tab = new GalleryTab(profile, parent); - if (tab->read(info, preload)) + if (tab->read(info, preload)) { return tab; + } } return nullptr; @@ -127,14 +118,12 @@ SearchTab *TabsLoader::loadTab(QJsonObject info, Profile *profile, MainWindow *p bool TabsLoader::save(const QString &path, QList &allTabs, SearchTab *currentTab) { QFile saveFile(path); - if (!saveFile.open(QFile::WriteOnly)) - { + if (!saveFile.open(QFile::WriteOnly)) { return false; } QJsonArray tabsJson; - for (auto tab : allTabs) - { + for (auto tab : allTabs) { QJsonObject tabJson; tab->write(tabJson); tabsJson.append(tabJson); diff --git a/gui/src/tabs/tag-tab.cpp b/gui/src/tabs/tag-tab.cpp index aae99d69d..c907e0a84 100644 --- a/gui/src/tabs/tag-tab.cpp +++ b/gui/src/tabs/tag-tab.cpp @@ -87,11 +87,11 @@ void TagTab::load() QString search = m_search->toPlainText().trimmed(); // Search an image directly by typing its MD5 - if (m_settings->value("enable_md5_fast_search", true).toBool()) - { + if (m_settings->value("enable_md5_fast_search", true).toBool()) { static QRegularExpression md5Matcher("^[0-9A-F]{32}$", QRegularExpression::CaseInsensitiveOption); - if (md5Matcher.match(search).hasMatch()) + if (md5Matcher.match(search).hasMatch()) { search.prepend("md5:"); + } } const QStringList tags = search.split(" ", QString::SkipEmptyParts); @@ -108,10 +108,20 @@ void TagTab::write(QJsonObject &json) const json["postFiltering"] = QJsonArray::fromStringList(m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts)); json["mergeResults"] = ui->checkMergeResults->isChecked(); + // Last urls + QJsonObject lastUrls; + for (const QString &site : m_pages.keys()) { + if (!m_pages[site].isEmpty()) { + lastUrls.insert(site, m_pages[site].last()->url().toString()); + } + } + json["lastUrls"] = lastUrls; + // Sites QJsonArray sites; - for (Site *site : loadSites()) + for (Site *site : loadSites()) { sites.append(site->url()); + } json["sites"] = sites; } @@ -122,32 +132,43 @@ bool TagTab::read(const QJsonObject &json, bool preload) ui->spinColumns->setValue(json["columns"].toInt()); ui->checkMergeResults->setChecked(json["mergeResults"].toBool()); + // Last urls + QJsonObject jsonLastUrls = json["lastUrls"].toObject(); + for (const QString &key : jsonLastUrls.keys()) { + m_lastUrls[key] = jsonLastUrls[key].toString(); + } + // Post filtering QJsonArray jsonPostFilters = json["postFiltering"].toArray(); QStringList postFilters; postFilters.reserve(jsonPostFilters.count()); - for (auto tag : jsonPostFilters) + for (auto tag : jsonPostFilters) { postFilters.append(tag.toString()); + } setPostFilter(postFilters.join(' ')); // Sources QJsonArray jsonSelectedSources = json["sites"].toArray(); QStringList selectedSources; selectedSources.reserve(jsonSelectedSources.count()); - for (auto site : jsonSelectedSources) + for (auto site : jsonSelectedSources) { selectedSources.append(site.toString()); + } QList selectedSourcesObj; - for (Site *site : m_sites) - if (selectedSources.contains(site->url())) + for (Site *site : m_sites) { + if (selectedSources.contains(site->url())) { selectedSourcesObj.append(site); + } + } saveSources(selectedSourcesObj, false); // Tags QJsonArray jsonTags = json["tags"].toArray(); QStringList tags; tags.reserve(jsonTags.count()); - for (auto tag : jsonTags) + for (auto tag : jsonTags) { tags.append(tag.toString()); + } setTags(tags.join(' '), preload); return true; @@ -156,72 +177,56 @@ bool TagTab::read(const QJsonObject &json, bool preload) void TagTab::setTags(const QString &tags, bool preload) { - activateWindow(); m_search->setText(tags); - if (preload) + if (preload) { + activateWindow(); load(); - else + } else { updateTitle(); + } } void TagTab::getPage() { - if (m_pages.empty()) + if (m_pages.empty()) { return; - - QStringList actuals, keys = m_sites.keys(); - for (int i = 0; i < m_checkboxes.count(); i++) - { - if (m_checkboxes.at(i)->isChecked()) - { actuals.append(keys.at(i)); } } + const bool unloaded = m_settings->value("getunloadedpages", false).toBool(); - for (int i = 0; i < actuals.count(); i++) - { - if (m_pages.contains(actuals[i])) - { - const auto &page = m_pages[actuals[i]].first(); - - const int perpage = unloaded ? ui->spinImagesPerPage->value() : (page->pageImageCount() > ui->spinImagesPerPage->value() ? page->pageImageCount() : ui->spinImagesPerPage->value()); - if (perpage <= 0 || page->pageImageCount() <= 0) - continue; - - const QString search = page->search().join(' '); - const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); - emit batchAddGroup(DownloadQueryGroup(m_settings, search, ui->spinPage->value(), perpage, perpage, postFiltering, m_sites.value(actuals.at(i)))); + + QList> pages = this->getPagesToDownload(); + for (const QSharedPointer &page : pages) { + const int perpage = unloaded ? ui->spinImagesPerPage->value() : (page->pageImageCount() > ui->spinImagesPerPage->value() ? page->pageImageCount() : ui->spinImagesPerPage->value()); + if (perpage <= 0 || page->pageImageCount() <= 0) { + continue; } + + const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); + + emit batchAddGroup(DownloadQueryGroup(m_settings, page->search(), ui->spinPage->value(), perpage, perpage, postFiltering, page->site())); } } void TagTab::getAll() { - if (m_pages.empty()) + if (m_pages.empty()) { return; - - QStringList actuals, keys = m_sites.keys(); - for (int i = 0; i < m_checkboxes.count(); i++) - { - if (m_checkboxes.at(i)->isChecked()) - actuals.append(keys.at(i)); } - for (const QString &actual : actuals) - { - const auto &page = m_pages[actual].first(); - + QList> pages = this->getPagesToDownload(); + for (const QSharedPointer &page : pages) { const int highLimit = page->highLimit(); const int currentCount = page->pageImageCount(); const int imageCount = page->imagesCount() >= 0 ? page->imagesCount() : page->maxImagesCount(); const int total = imageCount > 0 ? qMax(currentCount, imageCount) : (highLimit > 0 ? highLimit : currentCount); const int perPage = highLimit > 0 ? (imageCount > 0 ? qMin(highLimit, imageCount) : highLimit) : currentCount; - if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) + if ((perPage == 0 && total == 0) || (currentCount == 0 && imageCount <= 0)) { continue; + } - const QString search = page->search().join(' '); const QStringList postFiltering = m_postFiltering->toPlainText().split(' ', QString::SkipEmptyParts); - Site *site = m_sites.value(actual); - emit batchAddGroup(DownloadQueryGroup(m_settings, search, 1, perPage, total, postFiltering, site)); + emit batchAddGroup(DownloadQueryGroup(m_settings, page->search(), 1, perPage, total, postFiltering, page->site())); } } @@ -238,8 +243,7 @@ QString TagTab::tags() const void TagTab::changeEvent(QEvent *event) { // Automatically re-translate this tab on language change - if (event->type() == QEvent::LanguageChange) - { + if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } diff --git a/gui/src/tabs/tag-tab.ui b/gui/src/tabs/tag-tab.ui index f318c71ad..a0cd5db46 100644 --- a/gui/src/tabs/tag-tab.ui +++ b/gui/src/tabs/tag-tab.ui @@ -16,9 +16,6 @@ 0
- - New tab - :/images/icon.ico:/images/icon.ico diff --git a/gui/src/tag-context-menu.cpp b/gui/src/tag-context-menu.cpp index a581b412e..71e00ebff 100644 --- a/gui/src/tag-context-menu.cpp +++ b/gui/src/tag-context-menu.cpp @@ -12,45 +12,50 @@ TagContextMenu::TagContextMenu(QString tag, QList allTags, QUrl browserUrl, : QMenu(parent), m_tag(std::move(tag)), m_allTags(std::move(allTags)), m_browserUrl(std::move(browserUrl)), m_profile(profile) { // Favorites - if (profile->getFavorites().contains(Favorite(m_tag))) - { + if (profile->getFavorites().contains(Favorite(m_tag))) { addAction(QIcon(":/images/icons/remove.png"), tr("Remove from favorites"), this, SLOT(unfavorite())); - if (setImage) - { addAction(QIcon(":/images/icons/save.png"), tr("Choose as image"), this, SLOT(setfavorite())); } + if (setImage) { + addAction(QIcon(":/images/icons/save.png"), tr("Choose as image"), this, SLOT(setfavorite())); + } + } else { + addAction(QIcon(":/images/icons/add.png"), tr("Add to favorites"), this, SLOT(favorite())); } - else - { addAction(QIcon(":/images/icons/add.png"), tr("Add to favorites"), this, SLOT(favorite())); } // Keep for later - if (profile->getKeptForLater().contains(m_tag, Qt::CaseInsensitive)) - { addAction(QIcon(":/images/icons/remove.png"), tr("Don't keep for later"), this, SLOT(unviewitlater())); } - else - { addAction(QIcon(":/images/icons/add.png"), tr("Keep for later"), this, SLOT(viewitlater())); } + if (profile->getKeptForLater().contains(m_tag, Qt::CaseInsensitive)) { + addAction(QIcon(":/images/icons/remove.png"), tr("Don't keep for later"), this, SLOT(unviewitlater())); + } else { + addAction(QIcon(":/images/icons/add.png"), tr("Keep for later"), this, SLOT(viewitlater())); + } // Blacklist - if (profile->getBlacklist().contains(m_tag)) - { addAction(QIcon(":/images/icons/eye-plus.png"), tr("Don't blacklist"), this, SLOT(unblacklist())); } - else - { addAction(QIcon(":/images/icons/eye-minus.png"), tr("Blacklist"), this, SLOT(blacklist())); } + if (profile->getBlacklist().contains(m_tag)) { + addAction(QIcon(":/images/icons/eye-plus.png"), tr("Don't blacklist"), this, SLOT(unblacklist())); + } else { + addAction(QIcon(":/images/icons/eye-minus.png"), tr("Blacklist"), this, SLOT(blacklist())); + } // Ignore - if (profile->getIgnored().contains(m_tag, Qt::CaseInsensitive)) - { addAction(QIcon(":/images/icons/eye-plus.png"), tr("Don't ignore"), this, SLOT(unignore())); } - else - { addAction(QIcon(":/images/icons/eye-minus.png"), tr("Ignore"), this, SLOT(ignore())); } + if (profile->getIgnored().contains(m_tag, Qt::CaseInsensitive)) { + addAction(QIcon(":/images/icons/eye-plus.png"), tr("Don't ignore"), this, SLOT(unignore())); + } else { + addAction(QIcon(":/images/icons/eye-minus.png"), tr("Ignore"), this, SLOT(ignore())); + } addSeparator(); // Copy addAction(QIcon(":/images/icons/copy.png"), tr("Copy tag"), this, SLOT(copyTagToClipboard())); - if (!allTags.isEmpty()) - { addAction(QIcon(":/images/icons/copy.png"), tr("Copy all tags"), this, SLOT(copyAllTagsToClipboard())); } + if (!allTags.isEmpty()) { + addAction(QIcon(":/images/icons/copy.png"), tr("Copy all tags"), this, SLOT(copyAllTagsToClipboard())); + } addSeparator(); // Tabs addAction(QIcon(":/images/icons/tab-plus.png"), tr("Open in a new tab"), this, SLOT(openInNewTab())); addAction(QIcon(":/images/icons/window.png"), tr("Open in new a window"), this, SLOT(openInNewWindow())); - if (!browserUrl.isEmpty()) - { addAction(QIcon(":/images/icons/browser.png"), tr("Open in browser"), this, SLOT(openInBrowser())); } + if (!browserUrl.isEmpty()) { + addAction(QIcon(":/images/icons/browser.png"), tr("Open in browser"), this, SLOT(openInBrowser())); + } } void TagContextMenu::favorite() @@ -115,8 +120,9 @@ void TagContextMenu::copyAllTagsToClipboard() { QStringList tags; tags.reserve(m_allTags.count()); - for (const Tag &tag : qAsConst(m_allTags)) + for (const Tag &tag : qAsConst(m_allTags)) { tags.append(tag.text()); + } QApplication::clipboard()->setText(tags.join(' ')); } diff --git a/gui/src/theme-loader.cpp b/gui/src/theme-loader.cpp index 24d03c574..0ad95f72e 100644 --- a/gui/src/theme-loader.cpp +++ b/gui/src/theme-loader.cpp @@ -18,8 +18,9 @@ bool ThemeLoader::setTheme(const QString &name) QString dir = QString(m_path).replace('\\', '/') + name + "/"; QFile f(dir + "style.css"); - if (!f.open(QFile::ReadOnly | QFile::Text)) + if (!f.open(QFile::ReadOnly | QFile::Text)) { return false; + } QString css = f.readAll(); f.close(); diff --git a/gui/src/threads/image-loader-queue.cpp b/gui/src/threads/image-loader-queue.cpp index f8b4b407b..021ea7129 100644 --- a/gui/src/threads/image-loader-queue.cpp +++ b/gui/src/threads/image-loader-queue.cpp @@ -15,8 +15,7 @@ ImageLoaderQueue::ImageLoaderQueue(ImageLoader *imageLoader, QObject *parent) void ImageLoaderQueue::clear() { // If we have a loading waiting when we clear, we must not emit for it - if (m_waiting) - { + if (m_waiting) { m_cancelNext = true; } @@ -27,8 +26,7 @@ void ImageLoaderQueue::clear() void ImageLoaderQueue::load(const QByteArray &data) { // If we are already waiting for a loading, we queue this data - if (m_waiting) - { + if (m_waiting) { m_next = QByteArray(data); m_hasNext = true; return; @@ -41,8 +39,7 @@ void ImageLoaderQueue::load(const QByteArray &data) void ImageLoaderQueue::loadingSuccess(const QPixmap &pixmap, int size) { // We only emit the event if the loading was successful and not cancelled - if (!m_cancelNext) - { + if (!m_cancelNext) { emit finished(pixmap, size); } @@ -55,8 +52,7 @@ void ImageLoaderQueue::loadingFinished() m_cancelNext = false; // If we have some data in the queue, we load it directly - if (m_hasNext) - { + if (m_hasNext) { load(m_next); m_next = QByteArray(); m_hasNext = false; diff --git a/gui/src/threads/image-loader.cpp b/gui/src/threads/image-loader.cpp index 8d8c0b5bd..23d253d03 100644 --- a/gui/src/threads/image-loader.cpp +++ b/gui/src/threads/image-loader.cpp @@ -4,13 +4,14 @@ ImageLoader::ImageLoader(QObject *parent) : QObject(parent) -{ } +{} void ImageLoader::load(const QByteArray &data) { QPixmap img; - if (img.loadFromData(data)) + if (img.loadFromData(data)) { emit finished(img, data.size()); - else + } else { emit failed(); + } } diff --git a/gui/src/threads/resizer.cpp b/gui/src/threads/resizer.cpp index f1c29298c..9fd017998 100644 --- a/gui/src/threads/resizer.cpp +++ b/gui/src/threads/resizer.cpp @@ -4,17 +4,15 @@ Resizer::Resizer(QObject *parent) : QObject(parent), m_aspectMode(Qt::KeepAspectRatio) -{ } +{} void Resizer::run() { - if (!m_inputFilename.isEmpty()) - { + if (!m_inputFilename.isEmpty()) { m_input.load(m_inputFilename); } - if (m_input.isNull()) - { + if (m_input.isNull()) { emit error(); return; } diff --git a/gui/src/ui/QAffiche.cpp b/gui/src/ui/QAffiche.cpp index ef9a5a6a1..26899b59f 100644 --- a/gui/src/ui/QAffiche.cpp +++ b/gui/src/ui/QAffiche.cpp @@ -16,8 +16,7 @@ QAffiche::QAffiche(const QVariant &id, int border, QColor color, QWidget *parent void QAffiche::mouseDoubleClickEvent(QMouseEvent *e) { - if (e->button() == Qt::LeftButton) - { + if (e->button() == Qt::LeftButton) { emit doubleClicked(); emit doubleClicked(m_id.toInt()); } @@ -27,7 +26,7 @@ void QAffiche::mouseDoubleClickEvent(QMouseEvent *e) void QAffiche::mousePressEvent(QMouseEvent *e) { m_lastPressed = e->button(); - m_pressed = e->button() == Qt::LeftButton || e->button() == Qt::MidButton; + m_pressed = e->button() == Qt::LeftButton || e->button() == Qt::MidButton || e->button() == Qt::RightButton; emit pressed(); emit pressed(m_id.toInt()); QLabel::mousePressEvent(e); @@ -35,17 +34,18 @@ void QAffiche::mousePressEvent(QMouseEvent *e) void QAffiche::mouseReleaseEvent(QMouseEvent *e) { - if (m_pressed && e->button() == Qt::LeftButton && hitLabel(e->pos())) - { + if (m_pressed && e->button() == Qt::LeftButton && hitLabel(e->pos())) { emit clicked(); emit clicked(m_id.toInt()); emit clicked(m_id.toString()); - } - else if (m_pressed && e->button() == Qt::MidButton && hitLabel(e->pos())) - { + } else if (m_pressed && e->button() == Qt::MidButton && hitLabel(e->pos())) { emit middleClicked(); emit middleClicked(m_id.toInt()); emit middleClicked(m_id.toString()); + } else if (m_pressed && e->button() == Qt::RightButton && hitLabel(e->pos())) { + emit rightClicked(); + emit rightClicked(m_id.toInt()); + emit rightClicked(m_id.toString()); } m_pressed = false; emit released(); @@ -70,12 +70,10 @@ void QAffiche::leaveEvent(QEvent *e) void QAffiche::resizeEvent(QResizeEvent *e) { QMovie *mov = movie(); - if (mov != nullptr) - { + if (mov != nullptr) { const QSize &movieSize = mov->currentPixmap().size(); const QSize &newSize = e->size(); - if (newSize.width() < movieSize.width() || newSize.height() < movieSize.height()) - { + if (newSize.width() < movieSize.width() || newSize.height() < movieSize.height()) { mov->setScaledSize(movieSize.scaled(newSize, Qt::KeepAspectRatio)); } } diff --git a/gui/src/ui/QAffiche.h b/gui/src/ui/QAffiche.h index 1a69489d4..cf8c9c1fe 100644 --- a/gui/src/ui/QAffiche.h +++ b/gui/src/ui/QAffiche.h @@ -21,6 +21,9 @@ class QAffiche : public QLabel void clicked(); void clicked(int); void clicked(const QString &); + void rightClicked(); + void rightClicked(int); + void rightClicked(const QString &); void middleClicked(); void middleClicked(int); void middleClicked(const QString &); @@ -34,7 +37,7 @@ class QAffiche : public QLabel void mouseOut(int); protected: - //void paintEvent(QPaintEvent*); + // void paintEvent(QPaintEvent*); void mouseDoubleClickEvent(QMouseEvent*) override; void mousePressEvent(QMouseEvent*) override; void mouseReleaseEvent(QMouseEvent*) override; diff --git a/gui/src/ui/QBouton.cpp b/gui/src/ui/QBouton.cpp index 4fe452ae6..1844abd4c 100644 --- a/gui/src/ui/QBouton.cpp +++ b/gui/src/ui/QBouton.cpp @@ -6,18 +6,15 @@ QBouton::QBouton(QVariant id, bool resizeInsteadOfCropping, bool smartSizeHint, int border, QColor color, QWidget *parent) : QPushButton(parent), m_id(std::move(id)), m_resizeInsteadOfCropping(resizeInsteadOfCropping), m_smartSizeHint(smartSizeHint), m_penColor(std::move(color)), m_border(border), m_center(true), m_progress(0), m_progressMax(0), m_invertToggle(false), m_counter(QString()) -{ } +{} void QBouton::scale(const QPixmap &image, qreal scale) { QSize size; - if (scale - 1.0 > 0.001) - { + if (scale - 1.0 > 0.001) { size = image.size() * scale; setIcon(image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - } - else - { + } else { setIcon(image); size = image.size(); } @@ -46,8 +43,7 @@ void QBouton::setCounter(const QString &counter) void QBouton::paintEvent(QPaintEvent *event) { // Used for normal buttons - if (!m_resizeInsteadOfCropping && m_border == 0 && m_progressMax == 0 && m_counter.isEmpty()) - { + if (!m_resizeInsteadOfCropping && m_border == 0 && m_progressMax == 0 && m_counter.isEmpty()) { QPushButton::paintEvent(event); return; } @@ -62,25 +58,22 @@ void QBouton::paintEvent(QPaintEvent *event) int h = iconSize.height() + 2 * p; // Ignore invalid images - if (w == 0 || h == 0) + if (w == 0 || h == 0) { return; + } // Center the image - if (m_center) - { + if (m_center) { x += (region.width() - w) / 2; y += (region.height() - h) / 2; } // Draw image const QIcon::Mode mode = this->isChecked() ? QIcon::Selected : QIcon::Normal; - if (w > h) - { + if (w > h) { icon().paint(&painter, x + p, y + p, w - 2 * p, w - 2 * p, Qt::AlignLeft | Qt::AlignTop, mode); h = h - ((h * 2 * p) / w) + 2 * p - 1; - } - else - { + } else { icon().paint(&painter, x + p, y + p, h - 2 * p, h - 2 * p, Qt::AlignLeft | Qt::AlignTop, mode); w = w - ((w * 2 * p) / h) + 2 * p - 1; } @@ -89,8 +82,7 @@ void QBouton::paintEvent(QPaintEvent *event) painter.setClipRect(x, y, w, h); // Draw borders - if (p > 0 && m_penColor.isValid()) - { + if (p > 0 && m_penColor.isValid()) { QPen pen(m_penColor); pen.setWidth(p * 2); painter.setPen(pen); @@ -98,8 +90,7 @@ void QBouton::paintEvent(QPaintEvent *event) } // Draw progress - if (m_progressMax > 0 && m_progress > 0 && m_progress < m_progressMax) - { + if (m_progressMax > 0 && m_progress > 0 && m_progress < m_progressMax) { const int lineHeight = 6; const int a = p + lineHeight / 2; @@ -107,8 +98,7 @@ void QBouton::paintEvent(QPaintEvent *event) QPoint p1(qMax(x, 0) + a, qMax(y, 0) + a); QPoint p2(qFloor(p1.x() + (iconSize.width() - a) * ratio), p1.y()); - if (p2.x() > p1.x()) - { + if (p2.x() > p1.x()) { QPen pen(QColor(0, 200, 0)); pen.setWidth(lineHeight); painter.setPen(pen); @@ -117,8 +107,7 @@ void QBouton::paintEvent(QPaintEvent *event) } // Draw counter - if (!m_counter.isEmpty()) - { + if (!m_counter.isEmpty()) { const int right = qMax(x, 0) + qMin(w, size().width()); const int dim = 10 + 5 * m_counter.length(); const double pad = 2.5; @@ -145,12 +134,12 @@ QSize QBouton::getIconSize(int regionWidth, int regionHeight, bool wOnly) const int w = iconSize().width(); int h = iconSize().height(); - if (wOnly && w <= regionWidth) + if (wOnly && w <= regionWidth) { return iconSize(); + } // Calculate ratio to resize by keeping proportions - if (m_resizeInsteadOfCropping) - { + if (m_resizeInsteadOfCropping) { const qreal coef = wOnly ? qMin(1.0, static_cast(regionWidth) / static_cast(w)) : qMin(1.0, qMin(static_cast(regionWidth) / static_cast(w), static_cast(regionHeight) / static_cast(h))); @@ -165,15 +154,17 @@ void QBouton::resizeEvent(QResizeEvent *event) { QPushButton::resizeEvent(event); - if (m_smartSizeHint) + if (m_smartSizeHint) { updateGeometry(); + } } QSize QBouton::sizeHint() const { // Used for normal buttons - if (!m_smartSizeHint || (!m_resizeInsteadOfCropping && m_border == 0)) + if (!m_smartSizeHint || (!m_resizeInsteadOfCropping && m_border == 0)) { return QPushButton::sizeHint(); + } QSize current = size(); return getIconSize(current.width(), current.height(), true); @@ -190,38 +181,36 @@ void QBouton::mousePressEvent(QMouseEvent *event) if (pos.x() < wMargin || pos.y() < hMargin || pos.x() > imgSize.width() + wMargin - || pos.y() > imgSize.height() + hMargin) + || pos.y() > imgSize.height() + hMargin) { + event->ignore(); return; + } - if (event->button() == Qt::LeftButton) - { - const bool ctrlPressed = event->modifiers() & Qt::ControlModifier; - if (ctrlPressed != m_invertToggle) - { + if (event->button() == Qt::LeftButton) { + const bool ctrlPressed = event->modifiers().testFlag(Qt::ControlModifier); + if (ctrlPressed != m_invertToggle) { this->toggle(); - const bool range = event->modifiers() & Qt::ShiftModifier; + const bool range = event->modifiers().testFlag(Qt::ShiftModifier); emit this->toggled(m_id, this->isChecked(), range); emit this->toggled(m_id.toString(), this->isChecked(), range); emit this->toggled(m_id.toInt(), this->isChecked(), range); - } - else - { + } else { emit this->appui(m_id); emit this->appui(m_id.toString()); emit this->appui(m_id.toInt()); } - } - if (event->button() == Qt::RightButton) - { + } else if (event->button() == Qt::RightButton) { emit this->rightClick(m_id); emit this->rightClick(m_id.toString()); emit this->rightClick(m_id.toInt()); - } - if (event->button() == Qt::MidButton) - { + } else if (event->button() == Qt::MidButton) { emit this->middleClick(m_id); emit this->middleClick(m_id.toString()); emit this->middleClick(m_id.toInt()); + } else { + event->ignore(); + return; } + event->accept(); } diff --git a/gui/src/ui/click-menu.cpp b/gui/src/ui/click-menu.cpp index ea773b525..4b57d59a9 100644 --- a/gui/src/ui/click-menu.cpp +++ b/gui/src/ui/click-menu.cpp @@ -8,7 +8,7 @@ ClickMenu::ClickMenu(QWidget *parent) void ClickMenu::mouseReleaseEvent(QMouseEvent *event) { - QAction *action = actionAt(event->pos()); + QAction *action = activeAction(); if (action == nullptr) { QMenu::mouseReleaseEvent(event); return; diff --git a/gui/src/ui/fixed-size-grid-layout.cpp b/gui/src/ui/fixed-size-grid-layout.cpp index feaf0aabd..3a5724bcc 100644 --- a/gui/src/ui/fixed-size-grid-layout.cpp +++ b/gui/src/ui/fixed-size-grid-layout.cpp @@ -1,5 +1,5 @@ #include "fixed-size-grid-layout.h" -#include +#include FixedSizeGridLayout::FixedSizeGridLayout(QWidget *parent, int hSpacing, int vSpacing) @@ -13,8 +13,9 @@ FixedSizeGridLayout::FixedSizeGridLayout(int hSpacing, int vSpacing) FixedSizeGridLayout::~FixedSizeGridLayout() { QLayoutItem *item; - while ((item = takeAt(0)) != nullptr) + while ((item = takeAt(0)) != nullptr) { delete item; + } } @@ -33,8 +34,9 @@ void FixedSizeGridLayout::addItem(QLayoutItem *item) void FixedSizeGridLayout::insertItem(int index, QLayoutItem *item) { - if (index < 0) + if (index < 0) { index = m_items.count(); + } m_items.insert(index, item); invalidate(); @@ -58,8 +60,7 @@ QLayoutItem *FixedSizeGridLayout::itemAt(int index) const QLayoutItem *FixedSizeGridLayout::takeAt(int index) { - if (index >= 0 && index < m_items.size()) - { + if (index >= 0 && index < m_items.size()) { auto item = m_items.takeAt(index); invalidate(); return item; @@ -101,8 +102,9 @@ int FixedSizeGridLayout::heightForWidth(int width) const QSize FixedSizeGridLayout::minimumSize() const { QSize size; - for (QLayoutItem *item : m_items) + for (QLayoutItem *item : m_items) { size = size.expandedTo(item->minimumSize()); + } size += QSize(2 * margin(), 2 * margin()); return size; @@ -130,8 +132,7 @@ int FixedSizeGridLayout::doLayout(QRect rect, bool testOnly) const int w = effectiveRect.width(); int lineHeight = 0; - for (QLayoutItem *item : m_items) - { + for (QLayoutItem *item : m_items) { int spaceX = widgetSpacing(horizontalSpacing(), item->widget(), Qt::Horizontal); int spaceY = widgetSpacing(verticalSpacing(), item->widget(), Qt::Vertical); @@ -140,16 +141,16 @@ int FixedSizeGridLayout::doLayout(QRect rect, bool testOnly) const spaceX = qMax(spaceX, totalSpace / qMax(1, nbElements - 1)); int nextX = x + item->sizeHint().width() + spaceX; - if (nextX - spaceX - 1 > effectiveRect.right() && lineHeight > 0) - { + if (nextX - spaceX - 1 > effectiveRect.right() && lineHeight > 0) { x = effectiveRect.x(); y = y + lineHeight + spaceY; nextX = x + item->sizeHint().width() + spaceX; lineHeight = 0; } - if (!testOnly) + if (!testOnly) { item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + } x = nextX; lineHeight = qMax(lineHeight, item->sizeHint().height()); @@ -160,14 +161,15 @@ int FixedSizeGridLayout::doLayout(QRect rect, bool testOnly) const int FixedSizeGridLayout::smartSpacing(QStyle::PixelMetric pm) const { QObject *parent = this->parent(); - if (parent == nullptr) + if (parent == nullptr) { return -1; + } - if (parent->isWidgetType()) - { + if (parent->isWidgetType()) { auto *pw = dynamic_cast(parent); - if (pw != nullptr) + if (pw != nullptr) { return pw->style()->pixelMetric(pm, nullptr, pw); + } } return dynamic_cast(parent)->spacing(); @@ -175,8 +177,9 @@ int FixedSizeGridLayout::smartSpacing(QStyle::PixelMetric pm) const int FixedSizeGridLayout::widgetSpacing(int spacing, QWidget *widget, Qt::Orientation orientation) const { - if (spacing >= 0) + if (spacing >= 0) { return spacing; + } const QSizePolicy::ControlType controlType = widget->sizePolicy().controlType(); return widget->style()->layoutSpacing(controlType, controlType, orientation); diff --git a/gui/src/ui/fixed-size-grid-layout.h b/gui/src/ui/fixed-size-grid-layout.h index 67a08dcd3..a4ea5e91a 100644 --- a/gui/src/ui/fixed-size-grid-layout.h +++ b/gui/src/ui/fixed-size-grid-layout.h @@ -6,6 +6,9 @@ #include +class QWidget; +class QLayoutItem; + class FixedSizeGridLayout : public QLayout { Q_OBJECT diff --git a/gui/src/ui/qclosabletabwidget.cpp b/gui/src/ui/qclosabletabwidget.cpp index 14448bb60..ce7dcafe4 100644 --- a/gui/src/ui/qclosabletabwidget.cpp +++ b/gui/src/ui/qclosabletabwidget.cpp @@ -11,17 +11,14 @@ QClosableTabWidget::QClosableTabWidget(QWidget *parent) bool QClosableTabWidget::eventFilter(QObject *o, QEvent *e) { - if (o == tabBar() && e->type() == QEvent::MouseButtonPress) - { + if (o == tabBar() && e->type() == QEvent::MouseButtonPress) { auto *mouseEvent = dynamic_cast(e); - if (mouseEvent != nullptr && mouseEvent->button() == Qt::MiddleButton) - { + if (mouseEvent != nullptr && mouseEvent->button() == Qt::MiddleButton) { const int index = tabBar()->tabAt(mouseEvent->pos()); QWidget *w = widget(index); // Non-closable tabs have a maximum width of 16777214 (default: 16777215) - if (w->maximumWidth() != 16777214) - { + if (w->maximumWidth() != 16777214) { w->deleteLater(); removeTab(index); return true; diff --git a/gui/src/ui/tab-selector.cpp b/gui/src/ui/tab-selector.cpp index db28c29ba..21df1340e 100644 --- a/gui/src/ui/tab-selector.cpp +++ b/gui/src/ui/tab-selector.cpp @@ -97,7 +97,7 @@ void TabSelector::menuAboutToShow() void TabSelector::actionTriggered(QAction *action) { - QWidget *widget = action->data().value(); + auto *widget = action->data().value(); if (widget == nullptr) { return; } @@ -113,7 +113,7 @@ void TabSelector::actionTriggered(QAction *action) void TabSelector::actionTriggeredMiddle(QAction *action) { - QWidget *widget = action->data().value(); + auto *widget = action->data().value(); if (widget == nullptr) { return; } diff --git a/gui/src/ui/text-edit.cpp b/gui/src/ui/text-edit.cpp index 52ef73aa5..c978d3fd7 100644 --- a/gui/src/ui/text-edit.cpp +++ b/gui/src/ui/text-edit.cpp @@ -56,16 +56,18 @@ void TextEdit::doColor() fontFavorites.fromString(m_profile->getSettings()->value("Coloring/Fonts/favorites").toString()); const QString colorFavorites = m_profile->getSettings()->value("Coloring/Colors/favorites", "#ffc0cb").toString(); const QString styleFavorites = "color:" + colorFavorites + "; " + qFontToCss(fontFavorites); - for (const Favorite &fav : qAsConst(m_favorites)) + for (const Favorite &fav : qAsConst(m_favorites)) { txt.replace(" " + fav.getName() + " ", " " + fav.getName() + " "); + } // Color kept for later tags QFont fontKeptForLater; fontKeptForLater.fromString(m_profile->getSettings()->value("Coloring/Fonts/keptForLater").toString()); const QString colorKeptForLater = m_profile->getSettings()->value("Coloring/Colors/keptForLater", "#000000").toString(); const QString styleKeptForLater = "color:" + colorKeptForLater + "; " + qFontToCss(fontKeptForLater); - for (const QString &tag : qAsConst(m_viewItLater)) + for (const QString &tag : qAsConst(m_viewItLater)) { txt.replace(" " + tag + " ", " " + tag + " "); + } // Color metatags static QRegularExpression regexOr(" ~([^ ]+)"), @@ -82,14 +84,14 @@ void TextEdit::doColor() // Replace spaces to not be trimmed by the HTML renderer txt = txt.mid(1, txt.length() - 2); int depth = 0; - for (QChar &ch : txt) - { - if (ch == ' ' && depth == 0) + for (QChar &ch : txt) { + if (ch == ' ' && depth == 0) { ch = QChar(29); - else if (ch == '<') + } else if (ch == '<') { depth++; - else if (ch == '>') + } else if (ch == '>') { depth--; + } } txt.replace(QChar(29), " "); @@ -100,14 +102,11 @@ void TextEdit::doColor() const int end = crsr.selectionEnd(); setHtml(txt); - //If the cursor is at the right side of (if any) selected text - if (pos == end) - { + // If the cursor is at the right side of (if any) selected text + if (pos == end) { crsr.setPosition(start, QTextCursor::MoveAnchor); crsr.setPosition(end, QTextCursor::KeepAnchor); - } - else - { + } else { crsr.setPosition(end, QTextCursor::MoveAnchor); crsr.setPosition(start, QTextCursor::KeepAnchor); } @@ -126,12 +125,14 @@ void TextEdit::setText(const QString &text) void TextEdit::setCompleter(QCompleter *completer) { - if (completer == nullptr) + if (completer == nullptr) { return; + } // Disconnect the previous completer - if (c != nullptr) + if (c != nullptr) { QObject::disconnect(c, nullptr, this, nullptr); + } // Set the new completer and connect it to the field c = completer; @@ -148,8 +149,9 @@ QCompleter *TextEdit::completer() const void TextEdit::insertCompletion(const QString &completion) { - if (c->widget() != this) + if (c->widget() != this) { return; + } QTextCursor tc = textCursor(); const int extra = completion.length() - c->completionPrefix().length(); @@ -177,16 +179,16 @@ QString TextEdit::textUnderCursor() const void TextEdit::focusInEvent(QFocusEvent *e) { - if (c != nullptr) + if (c != nullptr) { c->setWidget(this); + } QTextEdit::focusInEvent(e); } void TextEdit::keyPressEvent(QKeyEvent *e) { - if (c != nullptr && c->popup()->isVisible()) - { + if (c != nullptr && c->popup()->isVisible()) { // The following keys are forwarded by the completer to the widget QString curr = c->popup()->currentIndex().data().toString(), under = textUnderCursor(); switch (e->key()) @@ -194,10 +196,9 @@ void TextEdit::keyPressEvent(QKeyEvent *e) case Qt::Key_Enter: case Qt::Key_Return: c->popup()->hide(); - if (curr.isEmpty() || under == curr) - { emit returnPressed(); } - else - { + if (curr.isEmpty() || under == curr) { + emit returnPressed(); + } else { insertCompletion(curr); doColor(); } @@ -211,11 +212,9 @@ void TextEdit::keyPressEvent(QKeyEvent *e) } } - const bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_Space); // CTRL+Space - if (c == nullptr || !isShortcut) // do not process the shortcut when we have a completer - { - if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) - { + const bool isShortcut = (e->modifiers().testFlag(Qt::ControlModifier) && e->key() == Qt::Key_Space); // CTRL+Space + if (c == nullptr || !isShortcut) { // do not process the shortcut when we have a completer + if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) { emit returnPressed(); return; } @@ -223,22 +222,23 @@ void TextEdit::keyPressEvent(QKeyEvent *e) } doColor(); - const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); - if (c == nullptr || (ctrlOrShift && e->text().isEmpty())) + const bool ctrlOrShift = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::ShiftModifier); + if (c == nullptr || (ctrlOrShift && e->text().isEmpty())) { return; + } static QString eow(" "); const bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift; QString completionPrefix = textUnderCursor(); - if (!isShortcut && (hasModifier || e->text().isEmpty() || completionPrefix.length() < 3 || eow.contains(e->text().right(1)))) - { + if (!isShortcut && (hasModifier || e->text().isEmpty() || completionPrefix.length() < 3 || eow.contains(e->text().right(1)))) { c->popup()->hide(); return; } - if (completionPrefix != c->completionPrefix()) + if (completionPrefix != c->completionPrefix()) { c->setCompletionPrefix(completionPrefix); + } QRect cr = cursorRect(); cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); @@ -254,14 +254,15 @@ void TextEdit::customContextMenuRequested(const QPoint &pos) auto *favsGroup = new QActionGroup(favs); favsGroup->setExclusive(true); connect(favsGroup, &QActionGroup::triggered, this, &TextEdit::insertFav); - for (const Favorite &fav : qAsConst(m_favorites)) - { favsGroup->addAction(fav.getName()); } - if (!toPlainText().isEmpty()) - { - if (m_favorites.contains(Favorite(toPlainText()))) - { favs->addAction(QIcon(":/images/icons/remove.png"), tr("Remove"), this, SLOT(unsetFavorite())); } - else - { favs->addAction(QIcon(":/images/icons/add.png"), tr("Add"), this, SLOT(setFavorite())); } + for (const Favorite &fav : qAsConst(m_favorites)) { + favsGroup->addAction(fav.getName()); + } + if (!toPlainText().isEmpty()) { + if (m_favorites.contains(Favorite(toPlainText()))) { + favs->addAction(QIcon(":/images/icons/remove.png"), tr("Remove"), this, SLOT(unsetFavorite())); + } else { + favs->addAction(QIcon(":/images/icons/add.png"), tr("Add"), this, SLOT(setFavorite())); + } favs->addSeparator(); } favs->addActions(favsGroup->actions()); @@ -272,14 +273,15 @@ void TextEdit::customContextMenuRequested(const QPoint &pos) auto *vilsGroup = new QActionGroup(vils); vilsGroup->setExclusive(true); connect(vilsGroup, &QActionGroup::triggered, this, &TextEdit::insertFav); - for (const QString &viewItLater : qAsConst(m_viewItLater)) - { vilsGroup->addAction(viewItLater); } - if (!toPlainText().isEmpty()) - { - if (m_viewItLater.contains(toPlainText())) - { vils->addAction(QIcon(":/images/icons/remove.png"), tr("Remove"), this, SLOT(unsetKfl())); } - else - { vils->addAction(QIcon(":/images/icons/add.png"), tr("Add"), this, SLOT(setKfl())); } + for (const QString &viewItLater : qAsConst(m_viewItLater)) { + vilsGroup->addAction(viewItLater); + } + if (!toPlainText().isEmpty()) { + if (m_viewItLater.contains(toPlainText())) { + vils->addAction(QIcon(":/images/icons/remove.png"), tr("Remove"), this, SLOT(unsetKfl())); + } else { + vils->addAction(QIcon(":/images/icons/add.png"), tr("Add"), this, SLOT(setKfl())); + } vils->addSeparator(); } vils->addActions(vilsGroup->actions()); @@ -316,8 +318,7 @@ void TextEdit::customContextMenuRequested(const QPoint &pos) sortings->setIcon(QIcon(":/images/sortings/sort.png")); menu->addMenu(sortings); menu->addSeparator(); - if (!textCursor().selection().isEmpty()) - { + if (!textCursor().selection().isEmpty()) { menu->addAction(tr("Copy"), this, SLOT(copy()), QKeySequence::Copy); menu->addAction(tr("Cut"), this, SLOT(cut()), QKeySequence::Cut); } @@ -351,16 +352,17 @@ void TextEdit::insertFav(QAction *act) int pos = cursor.columnNumber(); QString txt = this->toPlainText(); - if (!cursor.hasSelection()) - { - if (pos == 0 && (txt.count() == 0 || txt[0] != ' ')) + if (!cursor.hasSelection()) { + if (pos == 0 && txt.count() != 0 && txt[0] != ' ') { text.append(' '); - if (pos == txt.count() && txt[txt.count() - 1] != ' ') + } + if (pos == txt.count() && txt.count() != 0 && txt[txt.count() - 1] != ' ') { text.prepend(' '); + } this->setPlainText(txt.mid(0, pos) + text + txt.mid(pos)); + } else { + this->setPlainText(txt.mid(0, cursor.selectionStart()) + text + txt.mid(cursor.selectionEnd())); } - else - { this->setPlainText(txt.mid(0, cursor.selectionStart()) + text + txt.mid(cursor.selectionEnd())); } cursor.clearSelection(); cursor.setPosition(pos + text.length(), QTextCursor::KeepAnchor); diff --git a/gui/src/ui/verticalscrollarea.cpp b/gui/src/ui/verticalscrollarea.cpp index 95f26a99c..9ed95c28e 100644 --- a/gui/src/ui/verticalscrollarea.cpp +++ b/gui/src/ui/verticalscrollarea.cpp @@ -19,8 +19,9 @@ void VerticalScrollArea::resizeEvent(QResizeEvent *event) void VerticalScrollArea::setScrollEnabled(bool enabled) { - if (m_scrollEnabled == enabled) + if (m_scrollEnabled == enabled) { return; + } m_scrollEnabled = enabled; setVerticalScrollBarPolicy(enabled ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff); @@ -30,11 +31,11 @@ void VerticalScrollArea::setScrollEnabled(bool enabled) void VerticalScrollArea::updateWidgetSize() { QWidget *w = widget(); - if (w != nullptr) - { + if (w != nullptr) { int maxWidth = width(); - if (m_scrollEnabled && verticalScrollBar()->isVisible()) + if (m_scrollEnabled && verticalScrollBar()->isVisible()) { maxWidth -= verticalScrollBar()->width(); + } w->setMaximumWidth(maxWidth); w->setMaximumHeight(m_scrollEnabled ? QWIDGETSIZE_MAX : height()); @@ -45,17 +46,15 @@ void VerticalScrollArea::wheelEvent(QWheelEvent *e) { QScrollBar *scrollBar = verticalScrollBar(); - if (scrollBar->value() == scrollBar->maximum()) - { + if (scrollBar->value() == scrollBar->maximum()) { m_endOfScroll++; - if (m_endOfScroll == 3) - { + if (m_endOfScroll == 3) { m_endOfScroll = 0; emit endOfScrollReached(); } + } else { + m_endOfScroll = 0; } - else - { m_endOfScroll = 0; } QScrollArea::wheelEvent(e); } diff --git a/gui/src/updater/update-dialog.cpp b/gui/src/updater/update-dialog.cpp index e2bcabb80..faaee899e 100644 --- a/gui/src/updater/update-dialog.cpp +++ b/gui/src/updater/update-dialog.cpp @@ -9,7 +9,7 @@ UpdateDialog::UpdateDialog(bool *shouldQuit, QWidget *parent) - : QDialog(nullptr), ui(new Ui::UpdateDialog), m_shouldQuit(shouldQuit), m_parent(parent) + : QDialog(parent), ui(new Ui::UpdateDialog), m_shouldQuit(shouldQuit), m_parent(parent) { ui->setupUi(this); @@ -44,15 +44,13 @@ void UpdateDialog::checkForUpdates() void UpdateDialog::checkForUpdatesDone(const QString &newVersion, bool available, const QString &changelog) { - if (!available) - { + if (!available) { emit noUpdateAvailable(); return; } const bool hasChangelog = !changelog.isEmpty(); - if (hasChangelog) - { + if (hasChangelog) { ui->labelChangelog->setTextFormat(Qt::RichText); ui->labelChangelog->setText(parseMarkdown(changelog)); } @@ -96,8 +94,9 @@ void UpdateDialog::downloadFinished(const QString &path) QProcess::startDetached(path); - if (m_parent != nullptr) - { m_parent->close(); } + if (m_parent != nullptr) { + m_parent->close(); + } *m_shouldQuit = true; qApp->exit(); diff --git a/gui/src/utils/blacklist-fix/blacklist-fix-1.cpp b/gui/src/utils/blacklist-fix/blacklist-fix-1.cpp index 40beb019a..9737ed954 100644 --- a/gui/src/utils/blacklist-fix/blacklist-fix-1.cpp +++ b/gui/src/utils/blacklist-fix/blacklist-fix-1.cpp @@ -5,8 +5,8 @@ #include #include #include -#include "helpers.h" #include "functions.h" +#include "helpers.h" #include "logger.h" #include "models/image.h" #include "models/page.h" @@ -48,16 +48,14 @@ void BlacklistFix1::on_buttonContinue_clicked() // Check that directory exists QDir dir(ui->lineFolder->text()); - if (!dir.exists()) - { + if (!dir.exists()) { error(this, tr("This directory does not exist.")); ui->buttonContinue->setEnabled(true); return; } // Make sure the input is valid - if (!ui->radioForce->isChecked() && !ui->lineFilename->text().contains("%md5%")) - { + if (!ui->radioForce->isChecked() && !ui->lineFilename->text().contains("%md5%")) { error(this, tr("If you want to get the MD5 from the filename, you have to include the %md5% token in it.")); ui->buttonContinue->setEnabled(true); return; @@ -66,25 +64,21 @@ void BlacklistFix1::on_buttonContinue_clicked() // Get all files from the destination directory QVector> files; QDirIterator it(dir, QDirIterator::Subdirectories); - while (it.hasNext()) - { + while (it.hasNext()) { it.next(); - if (!it.fileInfo().isDir()) - { + if (!it.fileInfo().isDir()) { int len = it.filePath().length() - dir.absolutePath().length() - 1; files.append(QPair(it.filePath().right(len), it.filePath())); } } // Parse all files - for (const QPair &file : files) - { + for (const QPair &file : files) { QString md5 = ui->radioForce->isChecked() ? getFileMd5(file.second) : getFilenameMd5(file.first, ui->lineFilename->text()); - if (!md5.isEmpty()) - { + if (!md5.isEmpty()) { QMap det; det.insert("md5", md5); det.insert("path", file.first); @@ -94,8 +88,7 @@ void BlacklistFix1::on_buttonContinue_clicked() } int response = QMessageBox::question(this, tr("Blacklist fixer"), tr("You are about to download information from %n image(s). Are you sure you want to continue?", "", m_details.size()), QMessageBox::Yes | QMessageBox::No); - if (response == QMessageBox::Yes) - { + if (response == QMessageBox::Yes) { // Show progress bar ui->progressBar->setValue(0); ui->progressBar->setMaximum(files.size()); @@ -107,28 +100,25 @@ void BlacklistFix1::on_buttonContinue_clicked() void BlacklistFix1::getAll(Page *p) { - if (p != nullptr && !p->images().empty()) - { + if (p != nullptr && !p->images().empty()) { QSharedPointer img = p->images().at(0); m_getAll[img->md5()].insert("tags", img->tagsString().join(" ")); ui->progressBar->setValue(ui->progressBar->value() + 1); p->deleteLater(); } - if (!m_details.empty()) - { + if (!m_details.empty()) { QMap det = m_details.takeFirst(); m_getAll.insert(det.value("md5"), det); Page *page = new Page(m_profile, m_sites.value(ui->comboSource->currentText()), m_sites.values(), QStringList("md5:" + det.value("md5")), 1, 1); connect(page, &Page::finishedLoading, this, &BlacklistFix1::getAll); page->load(); - } - else - { + } else { Blacklist blacklist; - for (const QString &tags : ui->textBlacklist->toPlainText().split("\n", QString::SkipEmptyParts)) - { blacklist.add(tags.trimmed().split(' ', QString::SkipEmptyParts)); } + for (const QString &tags : ui->textBlacklist->toPlainText().split("\n", QString::SkipEmptyParts)) { + blacklist.add(tags.trimmed().split(' ', QString::SkipEmptyParts)); + } BlacklistFix2 *bf2 = new BlacklistFix2(m_getAll.values(), blacklist); close(); diff --git a/gui/src/utils/blacklist-fix/blacklist-fix-2.cpp b/gui/src/utils/blacklist-fix/blacklist-fix-2.cpp index 4585bb34f..670d4b628 100644 --- a/gui/src/utils/blacklist-fix/blacklist-fix-2.cpp +++ b/gui/src/utils/blacklist-fix/blacklist-fix-2.cpp @@ -13,11 +13,9 @@ BlacklistFix2::BlacklistFix2(QList> details, Blacklist bl ui->tableWidget->setRowCount(m_details.size()); QStringList found = QStringList(), tags; m_previews.reserve(m_details.count()); - for (int i = 0; i < m_details.size(); i++) - { + for (int i = 0; i < m_details.size(); i++) { QString color = "blue"; - if (m_details.at(i).contains("tags")) - { + if (m_details.at(i).contains("tags")) { QMap tokens; tokens.insert("allos", Token(tags)); found = m_blacklist.match(tokens); @@ -46,10 +44,10 @@ BlacklistFix2::~BlacklistFix2() void BlacklistFix2::on_buttonSelectBlacklisted_clicked() { - for (int i = 0; i < ui->tableWidget->rowCount(); i++) - { - if (!ui->tableWidget->item(i, 3)->text().isEmpty()) - { ui->tableWidget->selectRow(i); } + for (int i = 0; i < ui->tableWidget->rowCount(); i++) { + if (!ui->tableWidget->item(i, 3)->text().isEmpty()) { + ui->tableWidget->selectRow(i); + } } } void BlacklistFix2::on_buttonCancel_clicked() @@ -63,11 +61,11 @@ void BlacklistFix2::on_buttonOk_clicked() QList selected = ui->tableWidget->selectedItems(); const int count = selected.size(); QSet toDelete = QSet(); - for (int i = 0; i < count; i++) - { toDelete.insert(selected.at(i)->row()); } + for (int i = 0; i < count; i++) { + toDelete.insert(selected.at(i)->row()); + } int rem = 0; - for (int i : toDelete) - { + for (int i : toDelete) { QFile::remove(m_details.at(ui->tableWidget->item(i - rem, 0)->text().toInt() - 1).value("path_full")); ui->tableWidget->removeRow(i - rem); rem++; diff --git a/gui/src/utils/empty-dirs-fix/empty-dirs-fix-1.cpp b/gui/src/utils/empty-dirs-fix/empty-dirs-fix-1.cpp index b1f9d97c6..a99e36018 100644 --- a/gui/src/utils/empty-dirs-fix/empty-dirs-fix-1.cpp +++ b/gui/src/utils/empty-dirs-fix/empty-dirs-fix-1.cpp @@ -26,8 +26,7 @@ void EmptyDirsFix1::next() QStringList dirs = mkList(QDir(ui->lineFolder->text())); // We don't continue if there were no folders found - if (dirs.isEmpty()) - { + if (dirs.isEmpty()) { QMessageBox::information(this, tr("Empty folders fixer"), tr("No empty folder found.")); close(); return; @@ -42,12 +41,12 @@ QStringList EmptyDirsFix1::mkList(const QDir &dir) { QStringList ret; QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (int i = 0; i < dirs.size(); i++) - { - if (isEmpty(QDir(dir.path() + "/" + dirs.at(i)))) - { ret.append(dir.path() + "/" + dirs.at(i)); } - else - { mkList(QDir(dir.path() + "/" + dirs.at(i))); } + for (int i = 0; i < dirs.size(); i++) { + if (isEmpty(QDir(dir.path() + "/" + dirs.at(i)))) { + ret.append(dir.path() + "/" + dirs.at(i)); + } else { + mkList(QDir(dir.path() + "/" + dirs.at(i))); + } } return ret; } @@ -55,12 +54,14 @@ QStringList EmptyDirsFix1::mkList(const QDir &dir) bool EmptyDirsFix1::isEmpty(const QDir &dir) { QStringList files = dir.entryList(QDir::Files); - if (!files.isEmpty()) - { return false; } + if (!files.isEmpty()) { + return false; + } QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); bool empty = true; - for (int i = 0; i < dirs.size(); i++) - { empty = empty && isEmpty(QDir(dir.path() + "/" + dirs.at(i))); } + for (int i = 0; i < dirs.size(); i++) { + empty = empty && isEmpty(QDir(dir.path() + "/" + dirs.at(i))); + } return empty; } diff --git a/gui/src/utils/empty-dirs-fix/empty-dirs-fix-2.cpp b/gui/src/utils/empty-dirs-fix/empty-dirs-fix-2.cpp index 26a541ce0..5eaccde17 100644 --- a/gui/src/utils/empty-dirs-fix/empty-dirs-fix-2.cpp +++ b/gui/src/utils/empty-dirs-fix/empty-dirs-fix-2.cpp @@ -9,8 +9,9 @@ EmptyDirsFix2::EmptyDirsFix2(const QStringList &folders, QWidget *parent) { ui->setupUi(this); - for (const QString &folder : folders) - { ui->listWidget->addItem(new QListWidgetItem(folder)); } + for (const QString &folder : folders) { + ui->listWidget->addItem(new QListWidgetItem(folder)); + } ui->listWidget->selectAll(); } EmptyDirsFix2::~EmptyDirsFix2() @@ -23,8 +24,9 @@ bool EmptyDirsFix2::removeDir(QString path) path = QDir::toNativeSeparators(path); QDir dir(path); QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (int i = 0; i < dirs.size(); i++) - { removeDir(path+"/"+dirs.at(i)); } + for (int i = 0; i < dirs.size(); i++) { + removeDir(path + "/" + dirs.at(i)); + } return QDir().rmdir(path); } @@ -33,20 +35,20 @@ void EmptyDirsFix2::deleteSel() QList sel = ui->listWidget->selectedItems(); QStringList folders; folders.reserve(sel.count()); - for (QListWidgetItem *s : sel) - { folders.append(s->text()); } + for (QListWidgetItem *s : sel) { + folders.append(s->text()); + } - if (folders.isEmpty()) - { + if (folders.isEmpty()) { QMessageBox::information(this, tr("Empty folders fixer"), tr("No folder selected.")); return; } const int response = QMessageBox::question(this, tr("Empty folders fixer"), tr("You are about to delete %n folder. Are you sure you want to continue?", "", folders.size()), QMessageBox::Yes | QMessageBox::No); - if (response == QMessageBox::Yes) - { - for (int i = 0; i < folders.size(); i++) - { removeDir(folders.at(i)); } + if (response == QMessageBox::Yes) { + for (int i = 0; i < folders.size(); i++) { + removeDir(folders.at(i)); + } close(); } } diff --git a/gui/src/utils/md5-fix/md5-fix-worker.cpp b/gui/src/utils/md5-fix/md5-fix-worker.cpp new file mode 100644 index 000000000..70204bd3c --- /dev/null +++ b/gui/src/utils/md5-fix/md5-fix-worker.cpp @@ -0,0 +1,36 @@ +#include "utils/md5-fix/md5-fix-worker.h" +#include +#include "functions.h" + + +void Md5FixWorker::doWork(const QString &d, const QString &format, const QStringList &suffixes, bool force) +{ + QDir dir(d); + + // Get all files from the destination directory + auto files = listFilesFromDirectory(dir, suffixes); + emit maximumSet(files.count()); + + int loaded = 0; + int total = 0; + + // Parse all files + for (const auto &file : files) { + const QString fileName = file.first; + const QString path = dir.absoluteFilePath(fileName); + + QString md5 = force + ? getFileMd5(path) + : getFilenameMd5(fileName, format); + + if (!md5.isEmpty()) { + emit md5Calculated(md5, path); + loaded++; + } + total++; + + emit valueSet(total); + } + + emit finished(loaded); +} diff --git a/gui/src/utils/md5-fix/md5-fix-worker.h b/gui/src/utils/md5-fix/md5-fix-worker.h new file mode 100644 index 000000000..30eb5c35c --- /dev/null +++ b/gui/src/utils/md5-fix/md5-fix-worker.h @@ -0,0 +1,21 @@ +#ifndef MD5_FIX_WORKER_H +#define MD5_FIX_WORKER_H + +#include + + +class Md5FixWorker : public QObject +{ + Q_OBJECT + + public slots: + void doWork(const QString &dir, const QString &filename, const QStringList &suffixes, bool force); + + signals: + void maximumSet(int max); + void valueSet(int value); + void md5Calculated(const QString &md5, const QString &path); + void finished(int loadedCount); +}; + +#endif // MD5_FIX_WORKER_H diff --git a/gui/src/utils/md5-fix/md5-fix.cpp b/gui/src/utils/md5-fix/md5-fix.cpp index f32e60f6f..338c738ee 100644 --- a/gui/src/utils/md5-fix/md5-fix.cpp +++ b/gui/src/utils/md5-fix/md5-fix.cpp @@ -22,12 +22,28 @@ Md5Fix::Md5Fix(Profile *profile, QWidget *parent) ui->lineSuffixes->setText(getExternalLogFilesSuffixes(profile->getSettings()).join(", ")); ui->progressBar->hide(); + m_worker = new Md5FixWorker(); + m_worker->moveToThread(&m_thread); + connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + connect(this, &Md5Fix::startWorker, m_worker, &Md5FixWorker::doWork); + connect(m_worker, &Md5FixWorker::maximumSet, this, &Md5Fix::workerMaximumSet); + connect(m_worker, &Md5FixWorker::valueSet, this, &Md5Fix::workerValueSet); + connect(m_worker, &Md5FixWorker::md5Calculated, this, &Md5Fix::workerMd5Calculated); + connect(m_worker, &Md5FixWorker::finished, this, &Md5Fix::workerFinished); + + m_thread.start(); + resize(size().width(), 0); } Md5Fix::~Md5Fix() { delete ui; + + m_thread.quit(); + m_thread.wait(); + + m_worker->deleteLater(); } void Md5Fix::cancel() @@ -36,22 +52,54 @@ void Md5Fix::cancel() close(); } +void Md5Fix::workerMaximumSet(int max) +{ + if (max > 0) { + ui->progressBar->setValue(0); + ui->progressBar->setMaximum(max); + ui->progressBar->show(); + } +} + +void Md5Fix::workerValueSet(int value) +{ + ui->progressBar->setValue(value); +} + +void Md5Fix::workerMd5Calculated(const QString &md5, const QString &path) +{ + m_profile->addMd5(md5, path); +} + +void Md5Fix::workerFinished(int loadedCount) +{ + // Hide progress bar + ui->progressBar->hide(); + ui->progressBar->setValue(0); + ui->progressBar->setMaximum(0); + + ui->buttonStart->setEnabled(true); + + m_profile->sync(); + + QMessageBox::information(this, tr("Finished"), tr("%n MD5(s) loaded", "", loadedCount)); +} + void Md5Fix::start() { ui->buttonStart->setEnabled(false); // Check that directory exists - QDir dir(ui->lineFolder->text()); - if (!dir.exists()) - { + QString dir = ui->lineFolder->text(); + if (!QDir(dir).exists()) { error(this, tr("This folder does not exist.")); ui->buttonStart->setEnabled(true); return; } // Make sure the input is valid - if (!ui->radioForce->isChecked() && !ui->lineFilename->text().contains("%md5%")) - { + bool force = ui->radioForce->isChecked(); + if (!force && !ui->lineFilename->text().contains("%md5%")) { error(this, tr("If you want to get the MD5 from the filename, you have to include the %md5% token in it.")); ui->buttonStart->setEnabled(true); return; @@ -59,48 +107,9 @@ void Md5Fix::start() // Suffixes QStringList suffixes = ui->lineSuffixes->text().split(','); - for (QString &suffix : suffixes) + for (QString &suffix : suffixes) { suffix = suffix.trimmed(); - - // Get all files from the destination directory - auto files = listFilesFromDirectory(dir, suffixes); - - int count = 0; - if (files.count() > 0) - { - // Show progress bar - ui->progressBar->setValue(0); - ui->progressBar->setMaximum(files.size()); - ui->progressBar->show(); - - // Parse all files - for (const auto &file : files) - { - const QString fileName = file.first; - const QString path = dir.absoluteFilePath(fileName); - - QString md5 = ui->radioForce->isChecked() - ? getFileMd5(path) - : getFilenameMd5(fileName, ui->lineFilename->text()); - - if (!md5.isEmpty()) - { - m_profile->addMd5(md5, path); - count++; - } - - ui->progressBar->setValue(ui->progressBar->value() + 1); - } } - // Hide progress bar - ui->progressBar->hide(); - ui->progressBar->setValue(0); - ui->progressBar->setMaximum(0); - - ui->buttonStart->setEnabled(true); - - m_profile->sync(); - - QMessageBox::information(this, tr("Finished"), tr("%n MD5(s) loaded", "", count)); + emit startWorker(dir, ui->lineFilename->text(), suffixes, force); } diff --git a/gui/src/utils/md5-fix/md5-fix.h b/gui/src/utils/md5-fix/md5-fix.h index 538a29520..8048f3851 100644 --- a/gui/src/utils/md5-fix/md5-fix.h +++ b/gui/src/utils/md5-fix/md5-fix.h @@ -2,6 +2,8 @@ #define MD5_FIX_H #include +#include +#include "md5-fix-worker.h" namespace Ui @@ -24,9 +26,20 @@ class Md5Fix : public QDialog void cancel(); void start(); + // Worker events + void workerMaximumSet(int max); + void workerValueSet(int value); + void workerMd5Calculated(const QString &md5, const QString &path); + void workerFinished(int loadedCount); + + signals: + void startWorker(const QString &dir, const QString &format, const QStringList &suffixes, bool force); + private: Ui::Md5Fix *ui; Profile *m_profile; + QThread m_thread; + Md5FixWorker *m_worker; }; #endif // MD5_FIX_H diff --git a/gui/src/utils/rename-existing/rename-existing-1.cpp b/gui/src/utils/rename-existing/rename-existing-1.cpp index da7c59a92..fdcfb488a 100644 --- a/gui/src/utils/rename-existing/rename-existing-1.cpp +++ b/gui/src/utils/rename-existing/rename-existing-1.cpp @@ -52,16 +52,14 @@ void RenameExisting1::on_buttonContinue_clicked() // Check that directory exists QDir dir(ui->lineFolder->text()); - if (!dir.exists()) - { + if (!dir.exists()) { error(this, tr("This folder does not exist.")); ui->buttonContinue->setEnabled(true); return; } // Make sure the input is valid - if (!ui->radioForce->isChecked() && !ui->lineFilenameOrigin->text().contains(QStringLiteral("%md5%"))) - { + if (!ui->radioForce->isChecked() && !ui->lineFilenameOrigin->text().contains(QStringLiteral("%md5%"))) { error(this, tr("If you want to get the MD5 from the filename, you have to include the %md5% token in it.")); ui->buttonContinue->setEnabled(true); return; @@ -69,15 +67,15 @@ void RenameExisting1::on_buttonContinue_clicked() // Suffixes QStringList suffixes = ui->lineSuffixes->text().split(','); - for (QString &suffix : suffixes) + for (QString &suffix : suffixes) { suffix = suffix.trimmed(); + } // Get all files from the destination directory auto files = listFilesFromDirectory(dir, suffixes); // Parse all files - for (const auto &file : files) - { + for (const auto &file : files) { const QString fileName = file.first; const QString path = dir.absoluteFilePath(fileName); @@ -85,17 +83,16 @@ void RenameExisting1::on_buttonContinue_clicked() ? getFileMd5(path) : getFilenameMd5(fileName, ui->lineFilenameOrigin->text()); - if (!md5.isEmpty()) - { + if (!md5.isEmpty()) { RenameExistingFile det; det.md5 = md5; det.path = QDir::toNativeSeparators(path); - if (!file.second.isEmpty()) - { + if (!file.second.isEmpty()) { QStringList children; children.reserve(file.second.count()); - for (const QString &child : file.second) - { children.append(QDir::toNativeSeparators(dir.absoluteFilePath(child))); } + for (const QString &child : file.second) { + children.append(QDir::toNativeSeparators(dir.absoluteFilePath(child))); + } det.children = children; } m_details.append(det); @@ -107,25 +104,21 @@ void RenameExisting1::on_buttonContinue_clicked() m_needDetails = m_filename.needExactTags(m_sites.value(ui->comboSource->currentText())); const int response = QMessageBox::question(this, tr("Rename existing images"), tr("You are about to download information from %n image(s). Are you sure you want to continue?", "", m_details.size()), QMessageBox::Yes | QMessageBox::No); - if (response == QMessageBox::Yes) - { + if (response == QMessageBox::Yes) { // Show progress bar ui->progressBar->setValue(0); ui->progressBar->setMaximum(m_details.size()); ui->progressBar->show(); loadNext(); - } - else - { + } else { ui->buttonContinue->setEnabled(true); } } void RenameExisting1::getAll(Page *p) { - if (p->images().isEmpty()) - { + if (p->images().isEmpty()) { log(tr("No image found when renaming image '%1'").arg(p->search().join(' ')), Logger::Warning); ui->progressBar->setValue(ui->progressBar->value() + 1); loadNext(); @@ -133,13 +126,10 @@ void RenameExisting1::getAll(Page *p) } const QSharedPointer img = p->images().at(0); - if (m_needDetails == 2 || (m_needDetails == 1 && img->hasUnknownTag())) - { + if (m_needDetails == 2 || (m_needDetails == 1 && img->hasUnknownTag())) { connect(img.data(), &Image::finishedLoadingTags, this, &RenameExisting1::getTags); img->loadDetails(); - } - else - { + } else { setImageResult(img.data()); } } @@ -152,7 +142,7 @@ void RenameExisting1::getTags() void RenameExisting1::setImageResult(Image *img) { - QStringList paths = img->path(ui->lineFilenameDestination->text(), ui->lineFolder->text(), 0, true, true, true, true); + QStringList paths = img->paths(ui->lineFilenameDestination->text(), ui->lineFolder->text(), 0); m_getAll[img->md5()].newPath = paths.first(); ui->progressBar->setValue(ui->progressBar->value() + 1); @@ -161,8 +151,7 @@ void RenameExisting1::setImageResult(Image *img) void RenameExisting1::loadNext() { - if (!m_details.isEmpty()) - { + if (!m_details.isEmpty()) { const RenameExistingFile det = m_details.takeFirst(); m_getAll.insert(det.md5, det); diff --git a/gui/src/utils/rename-existing/rename-existing-2.cpp b/gui/src/utils/rename-existing/rename-existing-2.cpp index 08ede32c5..a8622096e 100644 --- a/gui/src/utils/rename-existing/rename-existing-2.cpp +++ b/gui/src/utils/rename-existing/rename-existing-2.cpp @@ -3,6 +3,7 @@ #include #include #include "functions.h" +#include "logger.h" RenameExisting2::RenameExisting2(QList details, QString folder, QWidget *parent) @@ -14,10 +15,8 @@ RenameExisting2::RenameExisting2(QList details, QString fold int i = 0; ui->tableWidget->setRowCount(m_details.size()); - for (const RenameExistingFile &image : qAsConst(m_details)) - { - if (showThumbnails) - { + for (const RenameExistingFile &image : qAsConst(m_details)) { + if (showThumbnails) { QLabel *preview = new QLabel(); preview->setPixmap(QPixmap(image.path).scaledToHeight(50, Qt::SmoothTransformation)); m_previews.append(preview); @@ -25,14 +24,13 @@ RenameExisting2::RenameExisting2(QList details, QString fold } ui->tableWidget->setItem(i, 1, new QTableWidgetItem(image.path.right(image.path.length() - m_folder.length() - 1))); - if (image.path == image.newPath) - { + if (image.path == image.newPath) { auto item = new QTableWidgetItem("No change"); item->setForeground(Qt::red); ui->tableWidget->setItem(i, 2, item); + } else { + ui->tableWidget->setItem(i, 2, new QTableWidgetItem(image.newPath.right(image.newPath.length() - m_folder.length() - 1))); } - else - { ui->tableWidget->setItem(i, 2, new QTableWidgetItem(image.newPath.right(image.newPath.length() - m_folder.length() - 1))); } i++; } @@ -43,8 +41,9 @@ RenameExisting2::RenameExisting2(QList details, QString fold headerView->setSectionResizeMode(1, QHeaderView::Stretch); headerView->setSectionResizeMode(2, QHeaderView::Stretch); - if (!showThumbnails) + if (!showThumbnails) { ui->tableWidget->removeColumn(0); + } } RenameExisting2::~RenameExisting2() @@ -60,12 +59,12 @@ void RenameExisting2::on_buttonCancel_clicked() void RenameExisting2::deleteDir(const QString &path) { - if (path == m_folder) + if (path == m_folder) { return; + } QDir directory(path); - if (directory.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).count() == 0) - { + if (directory.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).count() == 0) { directory.removeRecursively(); const QString parent = path.left(path.lastIndexOf(QDir::separator())); @@ -76,26 +75,25 @@ void RenameExisting2::deleteDir(const QString &path) void RenameExisting2::on_buttonOk_clicked() { // Move all images - for (const RenameExistingFile &image : qAsConst(m_details)) - { + for (const RenameExistingFile &image : qAsConst(m_details)) { // Ignore images with no change in path - if (image.newPath.isEmpty() || image.newPath == image.path) + if (image.newPath.isEmpty() || image.newPath == image.path) { continue; + } // Create hierarchy const QString path = image.newPath.left(image.newPath.lastIndexOf(QDir::separator())); QDir directory(path); - if (!directory.exists()) - { + if (!directory.exists()) { QDir dir; - if (!dir.mkpath(path)) - { log(QStringLiteral("Could not create destination directory"), Logger::Error); } + if (!dir.mkpath(path)) { + log(QStringLiteral("Could not create destination directory"), Logger::Error); + } } // Move file QFile::rename(image.path, image.newPath); - for (const QString &child : image.children) - { + for (const QString &child : image.children) { const QString newPath = QString(child).replace(image.path, image.newPath); QFile::rename(child, newPath); } diff --git a/gui/src/utils/tag-loader/tag-loader.cpp b/gui/src/utils/tag-loader/tag-loader.cpp index bca6272da..05f9b34ab 100755 --- a/gui/src/utils/tag-loader/tag-loader.cpp +++ b/gui/src/utils/tag-loader/tag-loader.cpp @@ -16,11 +16,9 @@ TagLoader::TagLoader(Profile *profile, QWidget *parent) ui->setupUi(this); QStringList keys; - for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) - { + for (auto it = m_sites.constBegin(); it != m_sites.constEnd(); ++it) { Site *site = it.value(); - if (!getCompatibleApis(site).isEmpty()) - { + if (!getCompatibleApis(site).isEmpty()) { m_options.append(it.key()); keys.append(QString("%1 (%L2 tags)").arg(it.key()).arg(site->tagDatabase()->count())); } @@ -40,9 +38,11 @@ TagLoader::~TagLoader() QList TagLoader::getCompatibleApis(Site *site) const { QList apis; - for (Api *a : site->getApis()) - if (a->canLoadTags()) + for (Api *a : site->getApis()) { + if (a->canLoadTags()) { apis.append(a); + } + } return apis; } @@ -75,8 +75,7 @@ void TagLoader::start() QList allTags; QList tags; int page = 1; - while (!tags.isEmpty() || page == 1) - { + while (!tags.isEmpty() || page == 1) { // Load tags for the current page QEventLoop loop; auto *tagApi = new TagApi(m_profile, site, api, page, 500, this); diff --git a/gui/src/viewer/details-window.cpp b/gui/src/viewer/details-window.cpp index 6c9e8c394..158a36236 100644 --- a/gui/src/viewer/details-window.cpp +++ b/gui/src/viewer/details-window.cpp @@ -20,14 +20,10 @@ void DetailsWindow::setImage(const QSharedPointer &image) { clearLayout(ui->formLayout); - for (const QPair &row : image->detailsData()) - { - if (row.first.isEmpty() && row.second.isEmpty()) - { + for (const QPair &row : image->detailsData()) { + if (row.first.isEmpty() && row.second.isEmpty()) { ui->formLayout->addItem(new QSpacerItem(10, 10)); - } - else - { + } else { const auto label = new QLabel(QString("%1").arg(row.first), this); auto field = new QLabel(row.second, this); field->setWordWrap(true); diff --git a/gui/src/viewer/zoom-window.cpp b/gui/src/viewer/zoom-window.cpp index d4e87b8a5..197698cb9 100644 --- a/gui/src/viewer/zoom-window.cpp +++ b/gui/src/viewer/zoom-window.cpp @@ -1,16 +1,25 @@ #include "viewer/zoom-window.h" #include +#include +#include +#include #include #include #include #include +#include +#include +#include #include #include +#include +#include #include #include "downloader/image-downloader.h" #include "functions.h" #include "helpers.h" #include "image-context-menu.h" +#include "logger.h" #include "main-window.h" #include "models/filename.h" #include "models/filtering/post-filter.h" @@ -19,6 +28,7 @@ #include "models/profile.h" #include "models/site.h" #include "settings/options-window.h" +#include "tabs/search-tab.h" #include "tag-context-menu.h" #include "tags/tag.h" #include "tags/tag-stylist.h" @@ -28,10 +38,11 @@ #include "viewer/details-window.h" -ZoomWindow::ZoomWindow(QList> images, const QSharedPointer &image, Site *site, Profile *profile, MainWindow *parent) - : QWidget(nullptr, Qt::Window), m_parent(parent), m_profile(profile), m_favorites(profile->getFavorites()), m_viewItLater(profile->getKeptForLater()), m_ignore(profile->getIgnored()), m_settings(profile->getSettings()), ui(new Ui::ZoomWindow), m_site(site), m_timeout(300), m_tooBig(false), m_loadedImage(false), m_loadedDetails(false), m_finished(false), m_size(0), m_fullScreen(nullptr), m_isFullscreen(false), m_isSlideshowRunning(false), m_images(std::move(images)), m_displayImage(QPixmap()), m_displayMovie(nullptr), m_labelImageScaled(false) +ZoomWindow::ZoomWindow(QList> images, const QSharedPointer &image, Site *site, Profile *profile, MainWindow *parent, SearchTab *tab) + : QWidget(nullptr, Qt::Window), m_parent(parent), m_tab(tab), m_profile(profile), m_favorites(profile->getFavorites()), m_viewItLater(profile->getKeptForLater()), m_ignore(profile->getIgnored()), m_settings(profile->getSettings()), ui(new Ui::ZoomWindow), m_site(site), m_timeout(300), m_tooBig(false), m_loadedImage(false), m_loadedDetails(false), m_finished(false), m_size(0), m_fullScreen(nullptr), m_isFullscreen(false), m_isSlideshowRunning(false), m_images(std::move(images)), m_displayImage(QPixmap()), m_displayMovie(nullptr), m_labelImageScaled(false) { setAttribute(Qt::WA_DeleteOnClose); + connect(parent, &MainWindow::destroyed, this, &QWidget::deleteLater); ui->setupUi(this); m_pendingAction = PendingNothing; @@ -119,8 +130,7 @@ ZoomWindow::ZoomWindow(QList> images, const QSharedPointer // Background color QString bg = m_settings->value("imageBackgroundColor", "").toString(); - if (!bg.isEmpty()) - { + if (!bg.isEmpty()) { setStyleSheet("#zoomWindow, #scrollAreaWidgetContents { background-color:" + bg + "; }"); m_labelImage->setStyleSheet("background-color:" + bg); m_labelTagsLeft->setStyleSheet("background-color:" + bg); @@ -133,22 +143,18 @@ void ZoomWindow::go() { ui->labelPools->hide(); bool whitelisted = false; - if (!m_settings->value("whitelistedtags").toString().isEmpty()) - { + if (!m_settings->value("whitelistedtags").toString().isEmpty()) { QStringList whitelist = m_settings->value("whitelistedtags").toString().split(" ", QString::SkipEmptyParts); - for (const Tag &t : m_image->tags()) - { - if (whitelist.contains(t.text())) - { + for (const Tag &t : m_image->tags()) { + if (whitelist.contains(t.text())) { whitelisted = true; break; } } } - if (m_settings->value("autodownload", false).toBool() || (whitelisted && m_settings->value("whitelist_download", "image").toString() == "image")) - { saveImage(); } - - m_url = m_image->getDisplayableUrl(); + if (m_settings->value("autodownload", false).toBool() || (whitelisted && m_settings->value("whitelist_download", "image").toString() == "image")) { + saveImage(); + } auto *timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(update())); @@ -156,26 +162,22 @@ void ZoomWindow::go() m_resizeTimer = timer; QString pos = m_settings->value("tagsposition", "top").toString(); - if (pos == QLatin1String("auto")) - { - if (!m_image->size().isEmpty()) - { - if (static_cast(m_image->width()) / static_cast(m_image->height()) >= 4.0 / 3.0) - { pos = QStringLiteral("top"); } - else - { pos = QStringLiteral("left"); } + if (pos == QLatin1String("auto")) { + if (!m_image->size().isEmpty()) { + if (static_cast(m_image->width()) / static_cast(m_image->height()) >= 4.0 / 3.0) { + pos = QStringLiteral("top"); + } else { + pos = QStringLiteral("left"); + } + } else { + pos = QStringLiteral("top"); } - else - { pos = QStringLiteral("top"); } } - if (pos == QLatin1String("top")) - { + if (pos == QLatin1String("top")) { ui->widgetLeft->hide(); m_labelTagsTop->show(); - } - else - { + } else { m_labelTagsTop->hide(); m_labelTagsLeft->show(); ui->widgetLeft->show(); @@ -188,8 +190,9 @@ void ZoomWindow::go() connect(m_image.data(), &Image::finishedLoadingTags, this, &ZoomWindow::replyFinishedDetails, Qt::UniqueConnection); m_image->loadDetails(); - if (!m_isFullscreen) + if (!m_isFullscreen) { activateWindow(); + } } /** @@ -197,8 +200,9 @@ void ZoomWindow::go() */ ZoomWindow::~ZoomWindow() { - if (m_displayMovie != nullptr) + if (m_displayMovie != nullptr) { m_displayMovie->deleteLater(); + } m_labelTagsTop->deleteLater(); m_labelTagsLeft->deleteLater(); @@ -206,7 +210,7 @@ ZoomWindow::~ZoomWindow() // Quit threads m_imageLoaderQueueThread.quit(); - m_imageLoaderThread.wait(1000); + m_imageLoaderQueueThread.wait(1000); m_imageLoaderThread.quit(); m_imageLoaderThread.wait(1000); @@ -259,13 +263,12 @@ void ZoomWindow::showDetails() } void ZoomWindow::openUrl(const QString &url) -{ emit linkClicked(url); } +{ emit linkClicked(QUrl::fromPercentEncoding(url.toUtf8())); } void ZoomWindow::openPool(const QString &url) { - if (url.startsWith(QLatin1String("pool:"))) - { emit poolClicked(url.rightRef(url.length() - 5).toInt(), m_image->parentSite()->url()); } - else - { + if (url.startsWith(QLatin1String("pool:"))) { + emit poolClicked(url.rightRef(url.length() - 5).toInt(), m_image->parentSite()->url()); + } else { Page *p = new Page(m_profile, m_image->parentSite(), m_profile->getSites().values(), QStringList() << "id:" + url, 1, 1, QStringList(), false, this); connect(p, &Page::finishedLoading, this, &ZoomWindow::openPoolId); p->load(); @@ -273,8 +276,7 @@ void ZoomWindow::openPool(const QString &url) } void ZoomWindow::openPoolId(Page *p) { - if (p->images().empty()) - { + if (p->images().empty()) { p->deleteLater(); return; } @@ -294,35 +296,25 @@ void ZoomWindow::openPoolId(Page *p) void ZoomWindow::openSaveDir(bool fav) { // If the file was already saved, we focus on it - if (!m_source.isEmpty()) - { + if (!m_source.isEmpty()) { showInGraphicalShell(m_source); - } - else - { - QString path = m_settings->value("Save/path" + QString(fav ? "_favorites" : "")).toString().replace("\\", "/"); + } else { + const QString path = m_settings->value("Save/path" + QString(fav ? "_favorites" : "")).toString(); const QString fn = m_settings->value("Save/filename" + QString(fav ? "_favorites" : "")).toString(); - if (path.right(1) == "/") - { path = path.left(path.length() - 1); } - path = QDir::toNativeSeparators(path); - - const QStringList files = m_image->path(fn, path); - const QString file = files.empty() ? QString() : files.at(0); - const QString pth = file.section(QDir::separator(), 0, -2); - const QString url = path + QDir::separator() + pth; + const QStringList files = m_image->paths(fn, path, 0); + const QString url = !files.empty() ? files.first() : path; - QDir dir(url); - if (dir.exists()) - { showInGraphicalShell(url); } - else - { + QDir dir = QFileInfo(url).dir(); + if (dir.exists()) { + showInGraphicalShell(url); + } else { const int reply = QMessageBox::question(this, tr("Folder does not exist"), tr("The save folder does not exist yet. Create it?"), QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::Yes) - { + if (reply == QMessageBox::Yes) { QDir rootDir(path); - if (!rootDir.mkpath(pth)) - { error(this, tr("Error creating folder.\n%1").arg(url)); } + if (!rootDir.mkpath(dir.path())) { + error(this, tr("Error creating folder.\n%1").arg(url)); + } showInGraphicalShell(url); } } @@ -332,13 +324,14 @@ void ZoomWindow::openSaveDirFav() { openSaveDir(true); } void ZoomWindow::linkHovered(const QString &url) -{ m_link = url; } +{ m_link = QUrl::fromPercentEncoding(url.toUtf8()); } void ZoomWindow::contextMenu(const QPoint &pos) { Q_UNUSED(pos); - if (m_link.isEmpty()) + if (m_link.isEmpty()) { return; + } Page page(m_profile, m_site, QList() << m_site, QStringList() << m_link); auto *menu = new TagContextMenu(m_link, m_image->tags(), page.friendlyUrl(), m_profile, true, this); @@ -349,21 +342,19 @@ void ZoomWindow::contextMenu(const QPoint &pos) void ZoomWindow::openInNewTab() { - m_parent->addTab(m_link); + m_parent->addTab(m_link, false, true, m_tab); } void ZoomWindow::setfavorite() { - if (!m_loadedImage) + if (!m_loadedImage) { return; + } Favorite fav(m_link); const int pos = m_favorites.indexOf(fav); - if (pos >= 0) - { + if (pos >= 0) { m_favorites[pos].setImage(m_displayImage); - } - else - { + } else { fav.setImage(m_displayImage); m_favorites.append(fav); } @@ -373,7 +364,8 @@ void ZoomWindow::setfavorite() void ZoomWindow::load(bool force) { - log(QStringLiteral("Loading image from `%1`").arg(m_url.toString())); + const Image::Size size = m_image->preferredDisplaySize(); + log(QStringLiteral("Loading image from `%1`").arg(m_image->url(size).toString())); m_source.clear(); @@ -381,17 +373,11 @@ void ZoomWindow::load(bool force) ui->progressBarDownload->setValue(0); ui->progressBarDownload->show(); - if (m_image->shouldDisplaySample()) - { - m_saveUrl = m_image->url(); - m_image->setUrl(m_url); - } - ImageDownloader *dwl = m_imageDownloaders.value(m_image, nullptr); - if (dwl == nullptr) - { - const QString fn = QUuid::createUuid().toString().mid(1, 36) + ".%ext%"; - dwl = new ImageDownloader(m_profile, m_image, fn, m_profile->tempPath(), 1, false, false, true, this, false, true, force); + if (dwl == nullptr) { + const Filename fn = Filename(QUuid::createUuid().toString().mid(1, 36) + ".%ext%"); + const QStringList paths = fn.path(*m_image.data(), m_profile, m_profile->tempPath(), 1, Filename::ExpandConditionals | Filename::Path); + dwl = new ImageDownloader(m_profile, m_image, paths, 1, false, false, this, true, force, size); m_imageDownloaders.insert(m_image, dwl); } connect(dwl, &ImageDownloader::downloadProgress, this, &ZoomWindow::downloadProgress, Qt::UniqueConnection); @@ -399,8 +385,9 @@ void ZoomWindow::load(bool force) m_imageTime.start(); - if (!dwl->isRunning()) - { dwl->save(); } + if (!dwl->isRunning()) { + dwl->save(); + } } #define PERCENT 0.05 @@ -413,28 +400,24 @@ void ZoomWindow::downloadProgress(QSharedPointer img, qint64 bytesReceive ui->progressBarDownload->setValue(bytesReceived); const bool isAnimated = m_image->isVideo() || !m_isAnimated.isEmpty(); - if (!isAnimated && (m_imageTime.elapsed() > TIME || (bytesTotal > 0 && static_cast(bytesReceived) / bytesTotal > PERCENT))) - { + if (!isAnimated && (m_imageTime.elapsed() > TIME || (bytesTotal > 0 && static_cast(bytesReceived) / bytesTotal > PERCENT))) { m_imageTime.restart(); - emit loadImage(m_image->data()); + // FIXME: should read the file contents now that the image is not loaded in RAM anymore + // emit loadImage(m_image->data()); } } void ZoomWindow::display(const QPixmap &pix, int size) { - if (!pix.size().isEmpty() && size >= m_size) - { + if (!pix.size().isEmpty() && size >= m_size) { m_size = size; m_displayImage = pix; update(!m_finished); - if (!pix.size().isEmpty() && m_image->size().isEmpty()) - { - m_image->setSize(pix.size()); - updateWindowTitle(); - } + updateWindowTitle(); - if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) - { m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } + if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) { + m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } } } @@ -446,13 +429,13 @@ void ZoomWindow::replyFinishedDetails() colore(); // Show pool information - if (!m_image->pools().isEmpty()) - { + if (!m_image->pools().isEmpty()) { auto imgPools = m_image->pools(); QStringList pools; pools.reserve(imgPools.count()); - for (const Pool &p : imgPools) - { pools.append((p.previous() != 0 ? "< " : "")+""+p.name()+""+(p.next() != 0 ? " >" : "")); } + for (const Pool &p : imgPools) { + pools.append((p.previous() != 0 ? "< " : "") + "" + p.name() + "" + (p.next() != 0 ? " >" : "")); + } ui->labelPools->setText(pools.join(QStringLiteral("
"))); ui->labelPools->show(); } @@ -460,30 +443,29 @@ void ZoomWindow::replyFinishedDetails() m_isAnimated = m_image->isAnimated(); const QString path1 = m_settings->value("Save/path").toString().replace("\\", "/"); - const QStringList pth1s = m_image->path(m_settings->value("Save/filename").toString(), path1, 0, true, true, true, true); + const QStringList pth1s = m_image->paths(m_settings->value("Save/filename").toString(), path1, 0); QString source1; - for (const QString &pth1 : pth1s) - { + for (const QString &pth1 : pth1s) { QFile file(pth1); - if (file.exists()) + if (file.exists()) { source1 = file.fileName(); + } } const QString path2 = m_settings->value("Save/path_favorites").toString().replace("\\", "/"); - const QStringList pth2s = m_image->path(m_settings->value("Save/filename_favorites").toString(), path2, 0, true, true, true, true); + const QStringList pth2s = m_image->paths(m_settings->value("Save/filename_favorites").toString(), path2, 0); QString source2; - for (const QString &pth2 : pth2s) - { + for (const QString &pth2 : pth2s) { QFile file(pth2); - if (file.exists()) + if (file.exists()) { source2 = file.fileName(); + } } QString md5Exists = m_profile->md5Exists(m_image->md5()); // If the file already exists, we directly display it - if (!md5Exists.isEmpty() || !source1.isEmpty() || !source2.isEmpty()) - { + if (!md5Exists.isEmpty() || !source1.isEmpty() || !source2.isEmpty()) { m_source = !md5Exists.isEmpty() ? md5Exists : (!source1.isEmpty() ? source1 : source2); m_imagePath = m_source; m_image->setSavePath(m_source); @@ -496,7 +478,6 @@ void ZoomWindow::replyFinishedDetails() // Fix extension when it should be guessed const QString fext = m_source.section('.', -1); - m_url = setExtension(m_url, fext); m_image->setFileExtension(fext); m_finished = true; @@ -505,11 +486,8 @@ void ZoomWindow::replyFinishedDetails() draw(); } - - // If the image already has an associated file on disk - else if (!m_image->savePath().isEmpty() && QFile::exists(m_image->savePath())) - { + else if (!m_image->savePath().isEmpty() && QFile::exists(m_image->savePath())) { m_imagePath = m_image->savePath(); log(QStringLiteral("Image loaded from the file `%1`").arg(m_imagePath)); @@ -519,12 +497,8 @@ void ZoomWindow::replyFinishedDetails() draw(); } - // If the file does not exist, we have to load it - else - { - if (m_url.isEmpty()) - { m_url = m_image->url(); } + else { load(); } @@ -535,10 +509,9 @@ void ZoomWindow::colore() QStringList t = TagStylist(m_profile).stylished(m_image->tags(), m_settings->value("Zoom/showTagCount", false).toBool(), false, m_settings->value("Zoom/tagOrder", "type").toString()); const QString tags = t.join(' '); - if (ui->widgetLeft->isHidden()) - { m_labelTagsTop->setText(tags); } - else - { + if (ui->widgetLeft->isHidden()) { + m_labelTagsTop->setText(tags); + } else { m_labelTagsLeft->setText(t.join(QStringLiteral("
"))); ui->scrollArea->setMinimumWidth(m_labelTagsLeft->sizeHint().width() + ui->scrollArea->verticalScrollBar()->sizeHint().width()); } @@ -548,10 +521,11 @@ void ZoomWindow::colore() void ZoomWindow::setButtonState(bool fav, SaveButtonState state) { // Update state - if (fav) + if (fav) { m_saveButonStateFav = state; - else + } else { m_saveButonState = state; + } // Update actual button label QPushButton *button = fav ? ui->buttonSaveFav : ui->buttonSave; @@ -615,36 +589,30 @@ void ZoomWindow::setButtonState(bool fav, SaveButtonState state) } } -void ZoomWindow::replyFinishedZoom(const QSharedPointer &img, const QMap &result) +void ZoomWindow::replyFinishedZoom(const QSharedPointer &img, const QList &result) { - log(QStringLiteral("Image received from `%1`").arg(m_url.toString())); - Image::SaveResult res = result.first(); + ImageSaveResult res = result.first(); + log(QStringLiteral("Image received from `%1`").arg(img->url(res.size).toString())); ui->progressBarDownload->hide(); m_finished = true; - if (m_image->shouldDisplaySample()) - { m_image->setUrl(m_saveUrl); } - - if (res == 500) - { + if (res.result == 500) { m_tooBig = true; - if (!m_image->isVideo()) - { error(this, tr("File is too big to be displayed.\n%1").arg(m_image->url().toString())); } - } - else if (res == Image::SaveResult::NotFound) - { showLoadingError("Image not found."); } - else if (res == Image::SaveResult::NetworkError) - { showLoadingError("Error loading the image."); } - else if (res == Image::SaveResult::Error) - { showLoadingError("Error saving the image."); } - else - { - m_url = m_image->url(); - m_imagePath = result.firstKey(); + if (!m_image->isVideo()) { + error(this, tr("File is too big to be displayed.\n%1").arg(m_image->url().toString())); + } + } else if (res.result == Image::SaveResult::NotFound) { + showLoadingError("Image not found."); + } else if (res.result == Image::SaveResult::NetworkError) { + showLoadingError("Error loading the image."); + } else if (res.result == Image::SaveResult::Error) { + showLoadingError("Error saving the image."); + } else { + m_imagePath = res.path; m_loadedImage = true; - img->setTemporaryPath(m_imagePath); + img->setTemporaryPath(m_imagePath, res.size); updateWindowTitle(); pendingUpdate(); @@ -662,21 +630,23 @@ void ZoomWindow::showLoadingError(const QString &message) void ZoomWindow::pendingUpdate() { // If we don't want to save, nothing to do - if (m_pendingAction == PendingNothing) + if (m_pendingAction == PendingNothing) { return; + } // If the image is not even loaded, we cannot save it (unless it's a big file) - if (!m_loadedImage && !m_tooBig) + if (!m_loadedImage && !m_tooBig) { return; + } // If the image is loaded but we need their tags and we don't have them, we wait - if (m_pendingAction != PendingSaveAs) - { + if (m_pendingAction != PendingSaveAs) { const bool fav = m_pendingAction == PendingSaveFav; Filename fn(m_settings->value("Save/path" + QString(fav ? "_favorites" : "")).toString()); - if (!m_loadedDetails && fn.needExactTags(m_site) != 0) + if (!m_loadedDetails && fn.needExactTags(m_site) != 0) { return; + } } switch (m_pendingAction) @@ -699,18 +669,17 @@ void ZoomWindow::pendingUpdate() void ZoomWindow::draw() { // Videos don't get drawn - if (m_image->isVideo()) + if (m_image->isVideo()) { return; + } // GIF (using QLabel support for QMovie) - if (!m_isAnimated.isEmpty()) - { + if (!m_isAnimated.isEmpty()) { m_displayMovie = new QMovie(m_imagePath, m_isAnimated.toLatin1(), this); m_displayMovie->start(); const QSize &movieSize = m_displayMovie->currentPixmap().size(); const QSize &imageSize = m_labelImage->size(); - if (imageSize.width() < movieSize.width() || imageSize.height() < movieSize.height()) - { + if (imageSize.width() < movieSize.width() || imageSize.height() < movieSize.height()) { m_displayMovie->setScaledSize(movieSize.scaled(imageSize, Qt::KeepAspectRatio)); } m_labelImage->setMovie(m_displayMovie); @@ -718,19 +687,21 @@ void ZoomWindow::draw() m_displayImage = QPixmap(); - if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) - { m_fullScreen->setMovie(m_displayMovie); } + if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) { + m_fullScreen->setMovie(m_displayMovie); + } } - // Images - else - { + else { m_displayImage = QPixmap(); m_displayImage.load(m_imagePath); + + updateWindowTitle(); update(); - if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) - { m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } + if (m_isFullscreen && m_fullScreen != nullptr && m_fullScreen->isVisible()) { + m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } } } @@ -744,27 +715,26 @@ void ZoomWindow::update(bool onlySize, bool force) { // Update image alignment QString type; - if (m_image->isVideo()) - { type = "imagePositionVideo"; } - else if (!m_isAnimated.isEmpty()) - { type = "imagePositionAnimation"; } - else - { type = "imagePositionImage"; } + if (m_image->isVideo()) { + type = "imagePositionVideo"; + } else if (!m_isAnimated.isEmpty()) { + type = "imagePositionAnimation"; + } else { + type = "imagePositionImage"; + } m_labelImage->setAlignment(getAlignments(type)); // Only used for images - if (m_displayImage.isNull()) + if (m_displayImage.isNull()) { return; + } const bool needScaling = (m_displayImage.width() > m_labelImage->width() || m_displayImage.height() > m_labelImage->height()); - if (needScaling && (onlySize || m_loadedImage || force)) - { + if (needScaling && (onlySize || m_loadedImage || force)) { const Qt::TransformationMode mode = onlySize ? Qt::FastTransformation : Qt::SmoothTransformation; m_labelImage->setImage(m_displayImage.scaled(m_labelImage->width(), m_labelImage->height(), Qt::KeepAspectRatio, mode)); m_labelImageScaled = true; - } - else if (m_loadedImage || force || (m_labelImageScaled && !needScaling)) - { + } else if (m_loadedImage || force || (m_labelImageScaled && !needScaling)) { m_labelImage->setImage(m_displayImage); m_labelImageScaled = false; } @@ -785,8 +755,7 @@ Qt::Alignment ZoomWindow::getAlignments(const QString &type) void ZoomWindow::saveNQuit() { - if (!m_source.isEmpty()) - { + if (!m_source.isEmpty()) { close(); return; } @@ -798,8 +767,7 @@ void ZoomWindow::saveNQuit() } void ZoomWindow::saveNQuitFav() { - if (!m_source.isEmpty()) - { + if (!m_source.isEmpty()) { close(); return; } @@ -826,12 +794,14 @@ void ZoomWindow::saveImage(bool fav) case SaveButtonState::Delete: { - if (m_imagePath.isEmpty() || m_imagePath == m_source) - { m_imagePath = m_profile->tempPath() + QDir::separator() + QUuid::createUuid().toString().mid(1, 36) + "." + m_image->extension(); } - if (QFile::exists(m_imagePath)) - { QFile::remove(m_source); } - else - { QFile::rename(m_source, m_imagePath); } + if (m_imagePath.isEmpty() || m_imagePath == m_source) { + m_imagePath = m_profile->tempPath() + QDir::separator() + QUuid::createUuid().toString().mid(1, 36) + "." + m_image->extension(); + } + if (QFile::exists(m_imagePath)) { + QFile::remove(m_source); + } else { + QFile::rename(m_source, m_imagePath); + } m_image->setTemporaryPath(m_imagePath); m_source = ""; setButtonState(fav, SaveButtonState::Save); @@ -846,34 +816,35 @@ void ZoomWindow::saveImageFav() { saveImage(true); } void ZoomWindow::saveImageNow() { - if (m_pendingAction == PendingSaveAs) - { - if (QFile::exists(m_saveAsPending)) - { QFile::remove(m_saveAsPending); } + if (m_pendingAction == PendingSaveAs) { + if (QFile::exists(m_saveAsPending)) { + QFile::remove(m_saveAsPending); + } bool ok = QFile(m_imagePath).copy(m_saveAsPending); auto result = ok ? Image::SaveResult::Saved : Image::SaveResult::Error; - m_image->postSave(m_saveAsPending, result, true, true, 1); - saveImageNowSaved(m_image, QMap {{ m_saveAsPending, result }}); + const Image::Size size = Image::Size::Full; // FIXME: depends on the size of m_imagePath + m_image->postSave(m_saveAsPending, size, result, true, true, 1); + saveImageNowSaved(m_image, QList {{ m_saveAsPending, size, result }}); return; } const bool fav = m_pendingAction == PendingSaveFav; QString fn = m_settings->value("Save/filename" + QString(fav ? "_favorites" : "")).toString(); QString pth = m_settings->value("Save/path" + QString(fav ? "_favorites" : "")).toString().replace("\\", "/"); - if (pth.right(1) == "/") - { pth = pth.left(pth.length() - 1); } + if (pth.right(1) == "/") { + pth = pth.left(pth.length() - 1); + } - if (pth.isEmpty() || fn.isEmpty()) - { + if (pth.isEmpty() || fn.isEmpty()) { int reply; - if (pth.isEmpty()) - { reply = QMessageBox::question(this, tr("Error"), tr("You did not specified a save folder! Do you want to open the options window?"), QMessageBox::Yes | QMessageBox::No); } - else - { reply = QMessageBox::question(this, tr("Error"), tr("You did not specified a save format! Do you want to open the options window?"), QMessageBox::Yes | QMessageBox::No); } - if (reply == QMessageBox::Yes) - { + if (pth.isEmpty()) { + reply = QMessageBox::question(this, tr("Error"), tr("You did not specified a save folder! Do you want to open the options window?"), QMessageBox::Yes | QMessageBox::No); + } else { + reply = QMessageBox::question(this, tr("Error"), tr("You did not specified a save format! Do you want to open the options window?"), QMessageBox::Yes | QMessageBox::No); + } + if (reply == QMessageBox::Yes) { auto *options = new OptionsWindow(m_profile, parentWidget()); - //options->onglets->setCurrentIndex(3); + // options->onglets->setCurrentIndex(3); options->setWindowModality(Qt::ApplicationModal); options->show(); connect(options, SIGNAL(closed()), this, SLOT(saveImage())); @@ -881,23 +852,21 @@ void ZoomWindow::saveImageNow() return; } - auto downloader = new ImageDownloader(m_profile, m_image, fn, pth, 1, true, true, true, this, false); + auto downloader = new ImageDownloader(m_profile, m_image, fn, pth, 1, true, true, this, false); connect(downloader, &ImageDownloader::saved, this, &ZoomWindow::saveImageNowSaved); connect(downloader, &ImageDownloader::saved, downloader, &ImageDownloader::deleteLater); downloader->save(); } -void ZoomWindow::saveImageNowSaved(QSharedPointer img, const QMap &result) +void ZoomWindow::saveImageNowSaved(QSharedPointer img, const QList &result) { Q_UNUSED(img); const bool fav = m_pendingAction == PendingSaveFav; - for (auto it = result.constBegin(); it != result.constEnd(); ++it) - { - const Image::SaveResult res = it.value(); - m_source = it.key(); + for (const ImageSaveResult &res : result) { + m_source = res.path; - switch (res) + switch (res.result) { case Image::SaveResult::Saved: setButtonState(fav, SaveButtonState::Saved); @@ -930,8 +899,9 @@ void ZoomWindow::saveImageNowSaved(QSharedPointer img, const QMapvalue("Zoom/lastDir", "").toString(); QString path = QFileDialog::getSaveFileName(this, tr("Save image"), QDir::toNativeSeparators(lastDir + "/" + filename), "Images (*.png *.gif *.jpg *.jpeg)"); - if (!path.isEmpty()) - { + if (!path.isEmpty()) { path = QDir::toNativeSeparators(path); m_settings->setValue("Zoom/lastDir", path.section(QDir::separator(), 0, -2)); @@ -959,24 +928,27 @@ void ZoomWindow::saveImageAs() void ZoomWindow::toggleFullScreen() { - if (m_isFullscreen) + if (m_isFullscreen) { unfullScreen(); - else + } else { fullScreen(); + } } void ZoomWindow::fullScreen() { - if (!m_loadedImage && m_displayMovie == nullptr) + if (!m_loadedImage && m_displayMovie == nullptr) { return; + } m_fullScreen = new QAffiche(QVariant(), 0, QColor(), this); m_fullScreen->setStyleSheet("background-color: black"); m_fullScreen->setAlignment(Qt::AlignCenter); - if (!m_isAnimated.isEmpty()) - { m_fullScreen->setMovie(m_displayMovie); } - else - { m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } + if (!m_isAnimated.isEmpty()) { + m_fullScreen->setMovie(m_displayMovie); + } else { + m_fullScreen->setImage(m_displayImage.scaled(QApplication::desktop()->screenGeometry().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } m_fullScreen->setWindowFlags(Qt::Window); m_fullScreen->showFullScreen(); @@ -1014,13 +986,15 @@ void ZoomWindow::unfullScreen() void ZoomWindow::prepareNextSlide() { // Slideshow is only enabled in fullscreen - if (!m_isFullscreen) + if (!m_isFullscreen) { return; + } // If the slideshow is disabled const int interval = m_settings->value("slideshow", 0).toInt(); - if (interval <= 0) + if (interval <= 0) { return; + } // We make sure to wait to see the whole displayed item const qint64 additionalInterval = !m_isAnimated.isEmpty() @@ -1036,18 +1010,20 @@ void ZoomWindow::toggleSlideshow() { m_isSlideshowRunning = !m_isSlideshowRunning; - if (!m_isSlideshowRunning) + if (!m_isSlideshowRunning) { m_slideshow.stop(); - else + } else { prepareNextSlide(); + } } void ZoomWindow::resizeEvent(QResizeEvent *e) { - if (!m_resizeTimer->isActive()) - { m_timeout = qMin(500, qMax(50, (m_displayImage.width() * m_displayImage.height()) / 100000)); } + if (!m_resizeTimer->isActive()) { + m_timeout = qMin(500, qMax(50, (m_displayImage.width() * m_displayImage.height()) / 100000)); + } m_resizeTimer->stop(); m_resizeTimer->start(m_timeout); update(true); @@ -1063,8 +1039,7 @@ void ZoomWindow::closeEvent(QCloseEvent *e) m_image->abortTags(); - for (auto it = m_imageDownloaders.constBegin(); it != m_imageDownloaders.constEnd(); ++it) - { + for (auto it = m_imageDownloaders.constBegin(); it != m_imageDownloaders.constEnd(); ++it) { it.value()->abort(); it.value()->deleteLater(); } @@ -1081,16 +1056,17 @@ void ZoomWindow::showEvent(QShowEvent *e) void ZoomWindow::showThumbnail() { QSize size = m_image->size(); - if (size.isEmpty()) - { size = m_image->previewImage().size() * 2 * m_settings->value("thumbnailUpscale", 1.0).toDouble(); } + if (size.isEmpty()) { + size = m_image->previewImage().size() * 2 * m_settings->value("thumbnailUpscale", 1.0).toDouble(); + } // Videos get a static resizable overlay - if (m_image->isVideo()) - { + if (m_image->isVideo()) { // A video thumbnail should not be upscaled to more than three times its size QSize maxSize = QSize(500, 500) * m_settings->value("thumbnailUpscale", 1.0).toDouble(); - if (size.width() > maxSize.width() || size.height() > maxSize.height()) - { size.scale(maxSize, Qt::KeepAspectRatio); } + if (size.width() > maxSize.width() || size.height() > maxSize.height()) { + size.scale(maxSize, Qt::KeepAspectRatio); + } const QPixmap &base = m_image->previewImage(); QPixmap overlay = QPixmap(":/images/play-overlay.png"); @@ -1104,27 +1080,17 @@ void ZoomWindow::showThumbnail() m_displayImage = result; update(false, true); } - // Gifs get non-resizable thumbnails - else if (!m_isAnimated.isEmpty()) - { + else if (!m_isAnimated.isEmpty()) { m_labelImage->setPixmap(m_image->previewImage().scaled(size, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } - // Other images get a resizable thumbnail - else if (m_displayImage.isNull()) - { + else if (m_displayImage.isNull()) { m_displayImage = m_image->previewImage().scaled(size, Qt::IgnoreAspectRatio, Qt::FastTransformation); update(false, true); } } -void ZoomWindow::urlChanged(const QUrl &before, const QUrl &after) -{ - Q_UNUSED(before); - m_url = after; -} - void ZoomWindow::reuse(const QList> &images, const QSharedPointer &image, Site *site) { @@ -1144,38 +1110,42 @@ void ZoomWindow::load(const QSharedPointer &image) m_imagePath = ""; m_image = image; m_isAnimated = image->isAnimated(); - connect(m_image.data(), &Image::urlChanged, this, &ZoomWindow::urlChanged, Qt::UniqueConnection); m_size = 0; ui->labelLoadingError->hide(); // Show the thumbnail if the image was not already preloaded - if (isVisible()) - { showThumbnail(); } + if (isVisible()) { + showThumbnail(); + } // Preload and abort next and previous images const int preload = m_settings->value("preload", 0).toInt(); QSet preloaded; const int index = m_images.indexOf(m_image); - for (int i = index - preload - 1; i <= index + preload + 1; ++i) - { + for (int i = index - preload - 1; i <= index + preload + 1; ++i) { bool forAbort = i == index - preload - 1 || i == index + preload + 1; int pos = (i + m_images.count()) % m_images.count(); - if (pos < 0 || pos == index || pos > m_images.count() || preloaded.contains(pos)) + if (pos < 0 || pos == index || pos > m_images.count() || preloaded.contains(pos)) { continue; + } QSharedPointer img = m_images[pos]; bool downloaderExists = m_imageDownloaders.contains(img); - if (downloaderExists && forAbort) + if (downloaderExists && forAbort) { m_imageDownloaders[img]->abort(); - if (downloaderExists || forAbort || (!img->savePath().isEmpty() && QFile::exists(img->savePath()))) + } + if (downloaderExists || forAbort || (!img->savePath().isEmpty() && QFile::exists(img->savePath()))) { continue; + } preloaded.insert(pos); log(QStringLiteral("Preloading data for image #%1").arg(pos)); m_images[pos]->loadDetails(); - const QString fn = QUuid::createUuid().toString().mid(1, 36) + ".%ext%"; - auto dwl = new ImageDownloader(m_profile, img, fn, m_profile->tempPath(), 1, false, false, true, this, false); + const Filename fn = Filename(QUuid::createUuid().toString().mid(1, 36) + ".%ext%"); + const QStringList paths = fn.path(*img.data(), m_profile, m_profile->tempPath(), 1, Filename::ExpandConditionals | Filename::Path); + const Image::Size size = img->preferredDisplaySize(); + auto dwl = new ImageDownloader(m_profile, img, paths, 1, false, false, this, true, false, size); m_imageDownloaders.insert(img, dwl); dwl->save(); } @@ -1199,19 +1169,22 @@ void ZoomWindow::updateWindowTitle() infos.append(getExtension(m_image->fileUrl()).toUpper()); // Filesize - if (m_image->fileSize() != 0) + if (m_image->fileSize() != 0) { infos.append(formatFilesize(m_image->fileSize())); + } // Image size - if (!m_image->size().isEmpty()) + if (!m_image->size().isEmpty()) { infos.append(QStringLiteral("%1 x %2").arg(m_image->size().width()).arg(m_image->size().height())); + } // Update title if there are infos to show QString title; - if (infos.isEmpty()) + if (infos.isEmpty()) { title = tr("Image"); - else + } else { title = QString(tr("Image") + " (%1)").arg(infos.join(", ")); + } setWindowTitle(QStringLiteral("%1 - %2 (%3/%4)").arg(title, m_image->parentSite()->name(), QString::number(m_images.indexOf(m_image) + 1), QString::number(m_images.count()))); } @@ -1222,8 +1195,7 @@ int ZoomWindow::firstNonBlacklisted(int direction) index = (index + m_images.count() + direction) % m_images.count(); // Skip blacklisted images - while (!m_profile->getBlacklist().match(m_images[index]->tokens(m_profile)).isEmpty() && index != first) - { + while (!m_profile->getBlacklist().match(m_images[index]->tokens(m_profile)).isEmpty() && index != first) { index = (index + m_images.count() + direction) % m_images.count(); } @@ -1248,13 +1220,12 @@ void ZoomWindow::previous() void ZoomWindow::updateButtonPlus() { - ui->buttonPlus->setText(ui->buttonPlus->isChecked() ? "-" : "+"); + ui->buttonPlus->setText(QChar(ui->buttonPlus->isChecked() ? '-' : '+')); } void ZoomWindow::openFile(bool now) { - if (!now) - { + if (!now) { m_pendingAction = PendingOpen; pendingUpdate(); return; @@ -1266,8 +1237,7 @@ void ZoomWindow::openFile(bool now) void ZoomWindow::mouseReleaseEvent(QMouseEvent *e) { - if (e->button() == Qt::MiddleButton && m_settings->value("imageCloseMiddleClick", true).toBool()) - { + if (e->button() == Qt::MiddleButton && m_settings->value("imageCloseMiddleClick", true).toBool()) { close(); return; } @@ -1277,25 +1247,24 @@ void ZoomWindow::mouseReleaseEvent(QMouseEvent *e) void ZoomWindow::wheelEvent(QWheelEvent *e) { - if (m_settings->value("imageNavigateScroll", true).toBool()) - { + if (m_settings->value("imageNavigateScroll", true).toBool()) { // Ignore events triggered when reaching the bottom of the tag list - if (ui->scrollArea->underMouse()) + if (ui->scrollArea->underMouse()) { return; + } // Ignore events if we already got one less than 500ms ago - if (!m_lastWheelEvent.isNull() && m_lastWheelEvent.elapsed() <= 500) + if (!m_lastWheelEvent.isNull() && m_lastWheelEvent.elapsed() <= 500) { e->ignore(); + } m_lastWheelEvent.start(); const int angle = e->angleDelta().y(); - if (angle <= -120) // Scroll down - { + if (angle <= -120) { // Scroll down next(); return; } - if (angle >= 120) // Scroll up - { + if (angle >= 120) { // Scroll up previous(); return; } diff --git a/gui/src/viewer/zoom-window.h b/gui/src/viewer/zoom-window.h index 1fbf809b9..418c64105 100644 --- a/gui/src/viewer/zoom-window.h +++ b/gui/src/viewer/zoom-window.h @@ -3,8 +3,8 @@ #include #include -#include #include +#include "downloader/image-save-result.h" #include "models/favorite.h" #include "models/image.h" @@ -22,6 +22,7 @@ class DetailsWindow; class ImageDownloader; class ImageLoader; class ImageLoaderQueue; +class SearchTab; class ZoomWindow : public QWidget { @@ -49,7 +50,7 @@ class ZoomWindow : public QWidget PendingOpen, }; - ZoomWindow(QList> images, const QSharedPointer &image, Site *site, Profile *profile, MainWindow *parent); + ZoomWindow(QList> images, const QSharedPointer &image, Site *site, Profile *profile, MainWindow *parent, SearchTab *tab); ~ZoomWindow() override; void go(); void load(bool force = false); @@ -57,14 +58,14 @@ class ZoomWindow : public QWidget public slots: void update(bool onlySize = false, bool force = false); void replyFinishedDetails(); - void replyFinishedZoom(const QSharedPointer &img, const QMap &result); + void replyFinishedZoom(const QSharedPointer &img, const QList &result); void display(const QPixmap &, int); void saveNQuit(); void saveNQuitFav(); void saveImage(bool fav = false); void saveImageFav(); void saveImageNow(); - void saveImageNowSaved(QSharedPointer img, const QMap &result); + void saveImageNowSaved(QSharedPointer img, const QList &result); void saveImageAs(); void openUrl(const QString &); void openPool(const QString &); @@ -77,7 +78,6 @@ class ZoomWindow : public QWidget void setfavorite(); void downloadProgress(QSharedPointer img, qint64 bytesReceived, qint64 bytesTotal); void colore(); - void urlChanged(const QUrl &before, const QUrl &after); void showDetails(); void pendingUpdate(); void updateButtonPlus(); @@ -127,6 +127,7 @@ class ZoomWindow : public QWidget private: MainWindow *m_parent; + SearchTab *m_tab; Profile *m_profile; QList &m_favorites; QStringList &m_viewItLater; @@ -135,15 +136,11 @@ class ZoomWindow : public QWidget Ui::ZoomWindow *ui; DetailsWindow *m_detailsWindow; QSharedPointer m_image; - QMap regex, m_details; Site *m_site; int m_timeout; PendingAction m_pendingAction; bool m_pendingClose; bool m_tooBig, m_loadedImage, m_loadedDetails; - QString id; - QUrl m_url, m_saveUrl; - QString rating, score, user; QAffiche *m_labelTagsTop, *m_labelTagsLeft; QTimer *m_resizeTimer; QTime m_imageTime; diff --git a/languages/ChineseSimplified.ts b/languages/ChineseSimplified.ts index 67fdc478e..12f573300 100644 --- a/languages/ChineseSimplified.ts +++ b/languages/ChineseSimplified.ts @@ -28,12 +28,12 @@ 俄语翻译:Николай Тихонов。 - + Grabber is up to date Grabber 已经是最新版本 - + A new version is available: %1 有新版本可用:%1 @@ -89,47 +89,63 @@ 添加一张图片 - + Add 添加 - + Site 站点 - + Id Id - + Md5 Md5 - + Filename 文件名 - + + <i>One ID per line.</i> + + + + + + + + + + + + + <i>One MD5 per line.</i> + + + + Folder 文件夹 - + Browse 浏览 - + Choose a save folder 选择保存的文件夹 - + No image found. 找不到图片。 @@ -146,149 +162,149 @@ Batch download - 下载队列 + 下载队列 Batch - 队列 + 队列 Url - Url + Url Filesize - 文件大小 + 文件大小 Speed - 下载速度 + 下载速度 Progress - 进度 + 进度 Follow downloaded images - 跟随已下载的图片 + 跟随已下载的图片 Copy links to clipboard - 复制链接到剪贴板 + 复制链接到剪贴板 When the download is finished - 当下载完成时执行 + 当下载完成时执行 Do nothing - 什么都不做 + 什么都不做 Close window - 关闭窗口 + 关闭窗口 Open CD tray - 打开 CD 播放器 + 打开 CD 播放器 Open destination folder - 打开目标文件夹 + 打开目标文件夹 Play a sound - 播放提示音 + 播放提示音 Shutdown - 关机 + 关机 Remove - 移除 + 移除 Details - 详情 + 详情 - + Pause - 暂停 + 暂停 Skip - 跳过 + 跳过 - + Cancel - 取消 + 取消 - + Paused - 已暂停 + 已暂停 - + Resume - 恢复 + 恢复 - - + + h 'h' m 'm' s 's' - h 'h' m 'm' s 's' + h 'h' m 'm' s 's' - - + + m 'm' s 's' - m 'm' s 's' + m 'm' s 's' - - + + s 's' - s 's' + s 's' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - <b>平均速度:</b> %1 %2<br/><br/><b>已耗时间:</b> %3<br/><b>剩余时间:</b> %4 + <b>平均速度:</b> %1 %2<br/><br/><b>已耗时间:</b> %3<br/><b>剩余时间:</b> %4 - + Close - 关闭 + 关闭 BlacklistFix1 - + Blacklist fixer 黑名单修复 @@ -338,17 +354,17 @@ 取消 - + This directory does not exist. 这个目录不存在。 - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. 如果你想要从文件名获得 MD5,你必须先添加 %md5% 变量到文件名中。 - + You are about to download information from %n image(s). Are you sure you want to continue? 你将要下载 %n 张图片的信息。你确定要继续吗? @@ -408,32 +424,32 @@ <i>You can either use a token or tags as a condition.</i> - <i>你可以使用变量或标签来做条件。</i> + <i>你可以使用变量或标签来做条件。</i> Condition - 条件 + 条件 Filename - 文件名 + 文件名 Folder - 文件夹 + 文件夹 <i>Leave empty to use the default folder.</i> - <i>留空将使用默认文件夹。</i> + <i>留空将使用默认文件夹。</i> <i>Leave empty to use the default filename.</i> - <i>留空将使用默认文件名。</i> + <i>留空将使用默认文件名。</i> @@ -569,335 +585,347 @@ Downloads - 下载 + 下载 Groups (0/0) - 分组 (0/0) + 分组 (0/0) - + Tags - 标签 + 标签 Source - 来源 + 来源 Page - 页面 + 页面 Images per page - 每页图片数 + 每页图片数 Images limit - 图片数限制 + 图片数限制 - + Filename - 文件名 + 文件名 - + Folder - 文件夹 + 文件夹 Post-filtering - 后过滤 + 后过滤 Get blacklisted - 加入黑名单 + 加入黑名单 + Galleries count as one + + + + Progress - 进度 + 进度 - - + + Add - 添加 + 添加 - + Single images - 单图片 + 单图片 - + Id - Id + Id - + Md5 - Md5 + Md5 - + Rating - 评级 + 评级 - + Url - Url + Url - + Date - 日期 + 日期 - + Search - 搜索 + 搜索 - + Site - 站点 + 站点 - + Delete all - 删除所有 + 删除所有 - + Delete selected - 删除已选择的 + 删除已选择的 - + Download - + Download selected - 下载已选择的 + 下载已选择的 - + Move down - 下移 + 下移 - + Load - 加载 + 加载 - + Save - 保存 + 保存 - + Move up - 上移 + 上移 - + Confirmation - + Are you sure you want to clear your download list? - + This source is not valid. - + The image per page value must be greater or equal to 1. - 每页图片数必须大于等于 1。 + 每页图片数必须大于等于 1。 - + The image limit must be greater or equal to 0. - 图片数限制必须大于或等于 0. + 图片数限制必须大于或等于 0. - + Groups (%1/%2) - 群组 (%1/%2) + 群组 (%1/%2) - - - + + + Save link list - 保存链接列表 + 保存链接列表 - - + + Imageboard-Grabber links (*.igl) - Imageboard-Grabber 链接列表 (*.igl) + Imageboard-Grabber 链接列表 (*.igl) - + Link list saved successfully! - 链接列表保存成功! + 链接列表保存成功! - + Error opening file. - 打开文件时发生错误。 + 打开文件时发生错误。 - - - + + + Load link list - 载入链接列表 + 载入链接列表 - + Link list loaded successfully! - 链接列表导入完成! + 链接列表导入完成! - + Loading %n download(s) - + 正在载入 %n 个下载 - + You did not specify a save folder! - 你还没有指定保存文件夹! + 你还没有指定保存文件夹! - + You did not specify a filename! - 你还没有指定文件名! + 你还没有指定文件名! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Don't ask me again - + Logging in, please wait... - 登录中,请稍候... + 登录中,请稍候... - + Downloading pages, please wait... - 下载页面中,请稍候... + 下载页面中,请稍候... - + Preparing images, please wait... - 准备图片中,请稍候... + 准备图片中,请稍候... - + Downloading images... - 下载图片中... + 下载图片中... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + An error occured saving the image. %1 Please solve the issue before resuming the download. - 保存图片时出现错误。 + 保存图片时出现错误。 %1 请在恢复下载前修复这些问题。 - + Error - 错误 + 错误 - - + + Getting images - 获取图片 + 获取图片 - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) - 下载图片时出现错误。请问你想要重启这些图片的下载吗?(%1/%2) + 下载图片时出现错误。请问你想要重启这些图片的下载吗?(%1/%2) - + %n file(s) downloaded successfully. - + %n 个图片已成功下载。 - + %n file(s) ignored. - + %n 个文件被忽略。 - + %n file(s) already existing. - + %n 个文件已经存在。 - + %n file(s) not found on the server. - + %n 个文件在服务器上无法找到。 - + %n file(s) skipped. - + %n 个文件被跳过。 - - %n error(s). + + %n file(s) skipped from a previous download. + + + + + + %n error(s). + %n 个错误。 @@ -906,7 +934,7 @@ Please solve the issue before resuming the download. EmptyDirsFix1 - + Empty folders fixer 空文件夹修复 @@ -926,7 +954,7 @@ Please solve the issue before resuming the download. 取消 - + No empty folder found. @@ -935,8 +963,8 @@ Please solve the issue before resuming the download. EmptyDirsFix2 - - + + Empty folders fixer 空文件夹修复 @@ -956,12 +984,12 @@ Please solve the issue before resuming the download. 取消 - + No folder selected. - + You are about to delete %n folder. Are you sure you want to continue? 你将删除 %n 个文件夹。你确定要继续吗? @@ -973,67 +1001,67 @@ Please solve the issue before resuming the download. Edit a favorite - 修改收藏 + 修改收藏 General - 一般 + 一般 Tag corresponding to the favorite. It is not often useful to change it. - 标签和收藏相对应。大部分时候不需要修改。 + 标签和收藏相对应。大部分时候不需要修改。 Tag - 标签 + 标签 Between 0 and 100, the note can be used to sort the favorites in preference order. - 值在 0 到 100 之间,注释可以通过偏爱度来进行排序。 + 值在 0 到 100 之间,注释可以通过偏爱度来进行排序。 Note - 注释 + 注释 % - % + % Last time you clicked on "Mark as viewed". - 上一次你选择”标记为已看“。 + 上一次你选择”标记为已看“。 Last view - 上次查看 + 上次查看 yyyy/MM/dd HH:mm:ss - yyyy/MM/dd HH:mm:ss + yyyy/MM/dd HH:mm:ss Image whose icon will be displayed in the favorites list. - 在收藏列表显示的图标。 + 在收藏列表显示的图标。 Image - 图片 + 图片 Browse - 浏览 + 浏览 @@ -1058,148 +1086,149 @@ Please solve the issue before resuming the download. Source - 来源 + 来源 Delete - 删除 + 删除 - + Choose an image - 选择一个图片 + 选择一个图片 FavoritesTab - + + Favorites - 收藏 + 收藏 Sort by - 筛选 + 筛选 Name - 名称 + 名称 Note - 注释 + 注释 Last view - 上次查看 + 上次查看 Ascending - 升序 + 升序 Descending - 降序 + 降序 O&k - O&k + O&k Number of columns - 列数 + 列数 Post-filtering - 后过滤 + 后过滤 Images per page - 每页图片数 + 每页图片数 - + Back - 返回 + 返回 - + Mark as &viewed - 标记为&已看 + 标记为&已看 - + Get &selected - + Get this &page - 获取此&页面的图片 + 获取此&页面的图片 - + Get &all - 获取&所有图片 + 获取&所有图片 - + S&ources - &来源 + &来源 - + Merge results - 合并结果 + 合并结果 - + Mark all as vie&wed - 标记所有为 &已看 + 标记所有为 &已看 - + MM/dd/yyyy - MM/dd/yyyy + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - + <b>名称:</b> %1<br/><b>注释:</b> %2 %<br/><b>上次查看:</b> %3 - - + + No result since the %1 - 从 %1 开始没有结果 + 从 %1 开始没有结果 - - + + MM/dd/yyyy 'at' hh:mm - MM/dd/yyyy '于' hh:mm + MM/dd/yyyy '于' hh:mm - + Mark as viewed - 标记为已看 + 标记为已看 - + Are you sure you want to mark all your favorites as viewed? - 你确定要标记所有收藏为已看吗? + 你确定要标记所有收藏为已看吗? @@ -1220,12 +1249,12 @@ Please solve the issue before resuming the download. Javascript 命名方式 - + Warning 警告 - + You script contains error, are you sure you want to save it? 你的脚本包含错误,你确定要保存吗? @@ -1233,44 +1262,43 @@ Please solve the issue before resuming the download. GalleryTab - New pool tab - 添加新集合标签页 + 添加新集合标签页 - + O&k - O&k + O&k - + Post-filtering - 后过滤 + 后过滤 - + Number of columns - 列数 + 列数 - + Images per page - 每页图片数 + 每页图片数 - + Get &selected - + 获取 &已选择的图片 - + Get this &page - 获取此&页面的图片 + 获取此&页面的图片 - + Get &all - 获取&所有图片 + 获取&所有图片 @@ -1312,136 +1340,136 @@ Please solve the issue before resuming the download. 图片包含 "%1" - + <b>Tags:</b> %1<br/><br/> <b>标签:</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> <b>ID:</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Rating:</b> %1<br/> <b>分级:</b> %1<br/> - + <b>Score:</b> %1<br/> <b>得分:</b> %1<br/> - + <b>User:</b> %1<br/><br/> <b>用户:</b> %1<br/><br/> - + <b>Size:</b> %1 x %2<br/> <b>大小:</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> <b>文件大小:</b> %1 %2<br/> - + <b>Date:</b> %1 <b>日期:</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm 'MM/dd/yyyy' 于 'hh:mm - + <i>Unknown</i> <i>未知</i> - + yes - + no - + Tags 标签 - + ID ID - + MD5 MD5 - + Rating 评级 - + Score 得分 - + Author 作者 - + Date 日期 - + 'the' MM/dd/yyyy 'at' hh:mm 'the' MM/dd/yyyy 'at' hh:mm - + Size 大小 - + Filesize 文件大小 - + Page 页面 - + URL URL - + Source(s) - - + + 来源 @@ -1449,37 +1477,37 @@ Please solve the issue before resuming the download. 来源 - + Sample 样本 - + Thumbnail 缩略图 - + Parent 父级 - + yes (#%1) 是 (#%1) - + Comments 评论 - + Children 子级 - + Notes 注释 @@ -1497,7 +1525,7 @@ Please solve the issue before resuming the download. - + Search MD5 @@ -1507,17 +1535,17 @@ Please solve the issue before resuming the download. Log - 日志 + 日志 Clear log - 清除日志 + 清除日志 Open log - 打开日志 + 打开日志 @@ -1603,7 +1631,7 @@ Please solve the issue before resuming the download. Tags - 标签 + 标签 Source @@ -1628,7 +1656,7 @@ Please solve the issue before resuming the download. Folder - 文件夹 + 文件夹 Post-filtering @@ -1701,7 +1729,7 @@ Please solve the issue before resuming the download. Save - 保存 + 保存 Move up @@ -1722,156 +1750,157 @@ Please solve the issue before resuming the download. Help - 帮助 + 帮助 Tools - 工具 + 工具 View - 查看 + 查看 File - 文件 + 文件 Kept for later - 等下再看 + 等下再看 Favorites - 收藏 + 收藏 Name - 名称 + 名称 Note - 注释 + 注释 Last viewed - 上次查看 + 上次查看 Ascending - 升序 + 升序 Descending - 降序 + 降序 Wiki - Wiki + Wiki Destination - 目标文件名 + 目标文件名 Reset - 重置 + 重置 Options - 选项 + 选项 Ctrl+P - Ctrl+P + Ctrl+P Open destination folder - 打开目标文件夹 + 打开目标文件夹 Quit - 退出 + 退出 About Grabber - 关于 Grabber + 关于 Grabber About Qt - 关于 Qt + 关于 Qt + New tab - 新标签页 + 新标签页 Close tab - 关闭标签页 + 关闭标签页 Blacklist fixer - 黑名单修复 + 黑名单修复 Empty folders fixer - 空文件夹修复 + 空文件夹修复 New pool tab - 添加新集合标签页 + 添加新集合标签页 MD5 list fixer - MD5 列表修复 + MD5 列表修复 Open options folder - 打开选项文件夹 + 打开选项文件夹 Project website - 项目网站 + 项目网站 Report an issue - 报告问题 + 报告问题 Rename existing images - 重命名已有图片 + 重命名已有图片 @@ -1889,32 +1918,32 @@ Please solve the issue before resuming the download. - + No source found - 找不到来源 + 找不到来源 - + No source found. Do you have a configuration problem? Try to reinstall the program. - 找不到来源。是否有配置问题?尝试重新安装程序。 + 找不到来源。是否有配置问题?尝试重新安装程序。 - + &Quit - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - 看来上次程序没有正常结束。你想要恢复会话吗? + 看来上次程序没有正常结束。你想要恢复会话吗? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - 检测到你有安装 Firefox 插件 "Danbooru Downloader"。你想要导入它的设置吗? + 检测到你有安装 Firefox 插件 "Danbooru Downloader"。你想要导入它的设置吗? - + Don't ask me again @@ -1931,19 +1960,19 @@ Please solve the issue before resuming the download. 图片数限制必须大于或等于 0. - + MM/dd/yyyy - MM/dd/yyyy + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - <b>名称:</b> %1<br/><b>注释:</b> %2 %%<br/><b>上次查看:</b> %3 + <b>名称:</b> %1<br/><b>注释:</b> %2 %%<br/><b>上次查看:</b> %3 - + Are you sure you want to quit? - 你确定要退出吗? + 你确定要退出吗? Don't keep for later @@ -2060,7 +2089,7 @@ Please solve the issue before resuming the download. - + Choose a save folder @@ -2070,47 +2099,47 @@ Please solve the issue before resuming the download. Md5 list fixer - MD5 列表修复 + MD5 列表修复 This tool will clear your MD5 list and fill it again with the MD5 of the files found in the folder set below. - 这个工具会清空你的 MD5 列表并重新填入下面设置的目录中找到的 MD5。 + 这个工具会清空你的 MD5 列表并重新填入下面设置的目录中找到的 MD5。 Folder - 文件夹 + 文件夹 Force md5 calculation - 强制进行 md5 计算 + 强制进行 md5 计算 Get md5 in filename - 通过文件名获取 md5 + 通过文件名获取 md5 Filename - 文件名 + 文件名 %v/%m - %v/%m + %v/%m Start - 开始 + 开始 Cancel - 取消 + 取消 @@ -2118,24 +2147,24 @@ Please solve the issue before resuming the download. - + This folder does not exist. - 这个文件夹不存在。 + 这个文件夹不存在。 - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - 如果你想要从文件名获得 MD5,你必须先添加 %md5% 变量到文件名中。 + 如果你想要从文件名获得 MD5,你必须先添加 %md5% 变量到文件名中。 - + Finished - 已完成 + 已完成 - + %n MD5(s) loaded - + %n 个 MD5 被加载 @@ -2143,19 +2172,19 @@ Please solve the issue before resuming the download. MonitoringCenter - + New images found for tag '%1' on '%2' - + %n new image(s) found for tag '%1' on '%2' - + More than %n new image(s) found for tag '%1' on '%2' @@ -2172,33 +2201,33 @@ Please solve the issue before resuming the download. Options - 选项 + 选项 General - 一般 + 一般 Sources - 来源 + 来源 Save - 保存 + 保存 Filename - 文件名 + 文件名 Conditional filenames - 条件文件名 + 条件文件名 @@ -2206,247 +2235,233 @@ Please solve the issue before resuming the download. - Artist tags - 艺术家标签 + 艺术家标签 - Copyright tags - 版权标签 + 版权标签 - Character tags - 角色标签 - - - - Species tags - + 角色标签 - - Meta tags - - - - + Custom token - 自定义变量 + 自定义变量 - + Interface - 界面 + 界面 - + Search results - + Image window - + Coloring - 颜色 + 颜色 - + Margins and borders - 距离和边框 + 距离和边框 - + Log - 日志 + 日志 - + Blacklist - 黑名单 + 黑名单 - + Monitoring - + Proxy - 代理 + 代理 - + Web services - - + + Commands - 命令行 + 命令行 - - + + Database - 数据库 + 数据库 - + Language - 语言 + 语言 - + At start - 启动时 + 启动时 - + Do nothing - 什么都不做 + 什么都不做 - + Load first page - 载入第一页 + 载入第一页 - + Restore last session - 恢复上次会话 + 恢复上次会话 - + Check for updates - + Every time - + Once a day - + Once a week - + Once a month - + Never - + Whitelist - 白名单 + 白名单 - + Download - + Don't download automatically - 不要自动下载 + 不要自动下载 - + When loading image - 加载图片时 + 加载图片时 - + When loading thumbnail - 加载缩略图时 + 加载缩略图时 - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> - <i>如果图片有在白名单中的标签,启用这个选项将会自动下载。</i> + <i>如果图片有在白名单中的标签,启用这个选项将会自动下载。</i> - + Ignored tags - 忽略的标签 + 忽略的标签 - + <i>These tags will not be taken in account when saving image.</i> - <i>这些标签在保存图片时不会被处理。</i> + <i>这些标签在保存图片时不会被处理。</i> - + Download images containing blacklisted tags - 下载包含黑名单中标签的图片 + 下载包含黑名单中标签的图片 - + Adds - 自动添加标签 + 自动添加标签 - <i>These tags will be automatically added to every search.</i> - <i>每次搜索时这些标签都会被自动加入。</i> + <i>每次搜索时这些标签都会被自动加入。</i> - + Ask for confirmation before closing the window - 关闭窗口前确认 + 关闭窗口前确认 Images per page - 每页图片数 + 每页图片数 Number of columns - 列数 + 列数 Source 1 - 来源 1 + 来源 1 Source 2 - 来源 2 + 来源 2 Source 3 - 来源 3 + 来源 3 Source 4 - 来源 4 + 来源 4 Get more precise tags when searching images - 在搜索图片时获取更精确的标签 + 在搜索图片时获取更精确的标签 @@ -2454,7 +2469,7 @@ Please solve the issue before resuming the download. XML - XML + XML @@ -2462,7 +2477,7 @@ Please solve the issue before resuming the download. JSON - JSON + JSON @@ -2470,7 +2485,7 @@ Please solve the issue before resuming the download. Regex - 正则表达式 + 正则表达式 @@ -2478,32 +2493,32 @@ Please solve the issue before resuming the download. RSS - RSS + RSS Auto tag add - 自动添加标签 + 自动添加标签 Download original images - 下载原图 + 下载原图 Download sample on error - 发生错误时下载缩略图 + 发生错误时下载缩略图 Download images automatically - 自动下载图片 + 自动下载图片 Keep original creation date - 保留源创建时间 + 保留源创建时间 @@ -2513,50 +2528,50 @@ Please solve the issue before resuming the download. Folder - 文件夹 + 文件夹 Browse - 浏览 + 浏览 - + Favorites - 收藏 + 收藏 Simultaneous downloads - 同时下载数 + 同时下载数 When the download is finished - 当下载完成时执行 + 当下载完成时执行 Close window - 关闭窗口 + 关闭窗口 Open CD tray - 打开 CD 播放器 + 打开 CD 播放器 Play a sound - 播放提示音 + 播放提示音 Shutdown - 关机 + 关机 @@ -2567,12 +2582,12 @@ Please solve the issue before resuming the download. Copy - 复制 + 复制 Move - 移动 + 移动 @@ -2583,17 +2598,17 @@ Please solve the issue before resuming the download. Don't save - 不要保存 + 不要保存 <i>File's identity is based on the MD5 algorithm.</i> - <i>文件的身份识别基于 MD5 值。</i> + <i>文件的身份识别基于 MD5 值。</i> Automatic redownload - 自动重下载 + 自动重下载 @@ -2608,42 +2623,42 @@ Please solve the issue before resuming the download. Default - 默认 + 默认 Tags separator - 标签分隔器 + 标签分隔器 Replace spaces by underscores - 用下划线代替空格 + 用下划线代替空格 Replace JPEG by JPG - 替换 JPG 为 JPEG + 替换 JPG 为 JPEG Max length - 最大长度 + 最大长度 <i>If the filename length is greater than this number, it will be shortened. Leave it to 0 to use the default limit.</i> - <i>如果一个文件名长度比这个数字长,就会被缩短。输入 0 使用默认限制。</i> + <i>如果一个文件名长度比这个数字长,就会被缩短。输入 0 使用默认限制。</i> Add a conditional filename - 添加一个条件文件名 + 添加一个条件文件名 <i>Each time an image is saved, its information can be added to a separate text file for later processing or for organization purposes.</i> - <i>每当图片保存时,一个文本文件将储存在相同的地方,包含这个图片的标签。</i> + <i>每当图片保存时,一个文本文件将储存在相同的地方,包含这个图片的标签。</i> @@ -2651,715 +2666,703 @@ Please solve the issue before resuming the download. - - - - - If empty - 如果为空 + 如果为空 - - - - - Separator - 分割器 + 分割器 - - - - - If more than n tags - 如果多余 n 个标签 + 如果多余 n 个标签 - - - - - Keep n tags, then add - 保留 n 个标签,然后添加 + 保留 n 个标签,然后添加 - - - - - Replace all tags by - 用 ... 替换所有标签 + 用 ... 替换所有标签 - - - - - Keep n tags - 保留 n 个标签 + 保留 n 个标签 - - - - - Keep all tags - 保留所有标签 + 保留所有标签 - - - - - One file per tag - 每个标签只保存一个文件 + 每个标签只保存一个文件 + + + Original + 源文件名 - Use shortest if possible - 尽量缩短 + 尽量缩短 - + Add a custom token - + Theme - + Upscaling - 升级 + 升级 - + % - % + % - + Favorites display - 显示收藏 + 显示收藏 - + Image, name and details - 图片,名称和详情 + 图片,名称和详情 - + Image and name - 图片和名称 + 图片和名称 - + Image and details - 图片和详情 + 图片和详情 - + Name and details - 名称和详情 + 名称和详情 - + Image only - 只有图片 + 只有图片 - + Name only - 只有名字 + 只有名字 - + Details only - 只有详情 + 只有详情 - + Hide favorites - 隐藏收藏 + 隐藏收藏 - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> - <i>当到达特定图片数时,收藏列表将被隐藏。</i> + <i>当到达特定图片数时,收藏列表将被隐藏。</i> - + Source's type display - 来源类型显示 + 来源类型显示 - + Text - 文本 + 文本 - - - + + + Image - 图片 + 图片 - + Image and text - 图片和文本 + 图片和文本 - + Don't show - 不显示 + 不显示 - + Displayed letters - 显示的字母 + 显示的字母 - + Display n letters - 显示 n 个字母 + 显示 n 个字母 - + Before first dot - 在第一个点前 + 在第一个点前 - + Before last dot - 在最后一个点前 + 在最后一个点前 - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> - <i>在主页面 ”+“ 的来源复选框旁显示的字母数量.</i> + <i>在主页面 ”+“ 的来源复选框旁显示的字母数量.</i> - + Preload all tabs when restoring a previous session - + Use a scroll area - + Use a fixed-image-width layout - + Infinite scroll - + Disabled - + Button - + Scroll - + Remember page number when infinite scrolling - + Resize previews instead of cropping them - 重设预览图大小而不是裁剪 + 重设预览图大小而不是裁剪 - + Enable autocompletion - 启用自动补全 + 启用自动补全 - + Show warning if an incompatible modifier is found - 当检测到不兼容的修改时显示警告 + 当检测到不兼容的修改时显示警告 - + Show other warnings - 显示其它警告 + 显示其它警告 - + Download not loaded pages - 下载没有加载的页面 + 下载没有加载的页面 - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> - <i>如果你启用了这个选项,点击”获取此页“按钮将会对每页显示的图片数和页面数等等进行修改,无论他们是否已经被加载。</i> + <i>如果你启用了这个选项,点击”获取此页“按钮将会对每页显示的图片数和页面数等等进行修改,无论他们是否已经被加载。</i> - + Invert Click and Ctrl+Click actions - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + Tag list position - 标签列表位置 + 标签列表位置 - - - - + + + + Top - 顶部 + 顶部 - - - - + + + + Left - 左边 + 左边 - + Auto - 自动 + 自动 - + Preloading - + Slideshow - + s - s + s - + Middle click to close window - + Enable scroll wheel navigation - + Show tag count - + Tag order - - + + Type - 类型 + 类型 - + Name - 名称 + 名称 - + Count - + Image position - - - - - - + + + + + + Center - - - + + + Bottom - - - + + + Right - + Animation position - + Video position - + Background color - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color - 颜色 + 颜色 - + Use a single image window - + + Tags + 标签 + + + + <i>These tags and post-filters will be automatically added to every search.</i> + + + + + Post-filters + + + + + Send anonymous usage data + + + + + Use image samples + + + + Artists - 艺术家 - - - - - - - - - - - - - - + 艺术家 + + + + + + + + + + + + + + Font - 自提 + 自提 - + Circle - 圆圈 + 圆圈 - + Series - 系列 + 系列 - + Characters - 角色 + 角色 - + Models - 模型 + 模型 - + Generals - 一般 + 一般 - + Blacklisted - 已被加入黑名单 + 已被加入黑名单 - + Ignored - 已被忽略 + 已被忽略 - + Species - + Kept for later - 等下再看 + 等下再看 - + Metas - + Hosts - 主机 + 主机 - - + + Horizontal margins - 水平间距 + 水平间距 - - + + Borders - 边框 + 边框 - + Images - 图片 + 图片 - + Vertical margins - 垂直间距 + 垂直间距 - + Show log - 显示日志 + 显示日志 - + Blacklisted tags - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + Ignore images containing a blacklisted tag - 忽略有黑名单中的标签的图片 + 忽略有黑名单中的标签的图片 - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> - <i>如果这个选项被选中,所有包含有黑名单中的标签的图片都将不被显示。否则,在显示图片前将弹出提示。</i> + <i>如果这个选项被选中,所有包含有黑名单中的标签的图片都将不被显示。否则,在显示图片前将弹出提示。</i> - + Delay on startup - + s - + Tray icon - + Minimize to tray - + Close to tray - + Enable system tray icon - + Use proxy - 使用代理 + 使用代理 - + HTTP - HTTP + HTTP - + SOCKS v5 - SOCKS v5 + SOCKS v5 - - + + Host - 主机 + 主机 - + Port - 端口 + 端口 - - + + User - 用户名 + 用户名 - - + + Password - 密码 + 密码 - + Use system-wide proxy settings - + Add a web service - - + + Tag (after) - 标签(之前) + 标签(之前) - - + + Tag (before) - 标签(之后) + 标签(之后) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) - 额外标签:: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: 标签类型, "general", "artist", "copyright", "character", "model" 或 "photo_set"<br/><i>%number%</i>: 标签类型数 ( 0 到 6 之间) + 额外标签:: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: 标签类型, "general", "artist", "copyright", "character", "model" 或 "photo_set"<br/><i>%number%</i>: 标签类型数 ( 0 到 6 之间) - + Start - 开始 + 开始 - + End - 结束 + 结束 - + Credentials - 验证 + 验证 - + Driver - 驱动 + 驱动 - + Choose a save folder - + Choose a save folder for favorites - 选择收藏保存的目录 + 选择收藏保存的目录 - - + + Edit - - + + Remove - 移除 + 移除 - + Choose a color - 选择一个颜色 + 选择一个颜色 - + Choose a font - 选择一个字体 + 选择一个字体 - + An error occured creating the save folder. - 创建保存目录时出错。 + 创建保存目录时出错。 - + An error occured creating the favorites save folder. - 创建收藏保存目录时出错。 + 创建收藏保存目录时出错。 Page - + No valid source of the site returned result. 没有有效的站点返回数据来源。 @@ -3378,54 +3381,53 @@ Please solve the issue before resuming the download. PoolTab - New pool tab - 添加新集合标签页 + 添加新集合标签页 - + Pl&us - Pl&us + Pl&us - + O&k - O&k + O&k - + Maybe you meant: - 或许你的意思是: + 或许你的意思是: - + Images per page - 每页图片数 + 每页图片数 - + Number of columns - 列数 + 列数 - + Post-filtering - 后过滤 + 后过滤 - + Get &selected - + Get this &page - 获取此&页面的图片 + 获取此&页面的图片 - + Get &all - 获取&所有图片 + 获取&所有图片 @@ -3501,61 +3503,60 @@ Please solve the issue before resuming the download. <b>提示</b> %1 - MM-dd-yyyy HH.mm - MM-dd-yyyy HH.mm + MM-dd-yyyy HH.mm Error in Javascript evaluation:<br/> Javascript 测试失败:<br/> - + Filename must not be empty! 文件名不能为空! - + Can't validate Javascript expressions. 无法验证 Javascript 表达式。 - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. 你的文件名不以扩展名结束,请添加 %ext%!否则你可能无法打开保存的文件。 - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. 你的文件名不唯一,新图片可能会将之前的图片覆盖!你应该使用 %md5%,因为它是唯一的,可以避免不便。 - + The %%1% token does not exist and will not be replaced. %%1% 变量不存在,且不会被替换。 - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | 你的文件名格式存在 Windows 不允许的字符!不允许的字符: * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. 你选择使用 %id% 变量。它只在特定的站点唯一。相同的 ID 可能在其他站点代表不同的图片。 - + Valid filename! 文件名有效! - + image has a "%1" token - + image does not have a "%1" token @@ -3564,53 +3565,53 @@ Please solve the issue before resuming the download. 未知格式 "%1" (可用格式:"%2") - - - + + + image's %1 does not match 图片的 %1 不符合 - - - + + + image's %1 match 图片的 %1 符合 - - + + image is not "%1" 图片不是 "%1" - - + + image is "%1" 图片是 "%1" - + An image needs a date to be filtered by age - + image's source does not starts with "%1" 图片的来源不以 "%1" 开头 - + image's source starts with "%1" 图片的来源以 "%1" 开头 - + image does not contains "%1" 图片不包含 "%1" - + image contains "%1" 图片包含 "%1" @@ -3619,7 +3620,7 @@ Please solve the issue before resuming the download. RenameExisting1 - + Rename existing images 重命名已有图片 @@ -3674,24 +3675,24 @@ Please solve the issue before resuming the download. - + This folder does not exist. 这个文件夹不存在。 - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. 如果你想要从文件名获得 MD5,你必须先添加 %md5% 变量到文件名中。 - + You are about to download information from %n image(s). Are you sure you want to continue? 你将要下载 %n 张图片的信息。你确定要继续吗? - + No image found when renaming image '%1' @@ -3737,107 +3738,112 @@ Please solve the issue before resuming the download. SearchTab - + + all images filtered + + + + server offline - 服务器不在线 + 服务器不在线 - + too many tags - 太多标签 + 太多标签 - + page too far - 页面过多 + 页面过多 - + HTTPS redirection detected - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Always - + Never for that website - + Never - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - 这个图片中的一些标签是白名单中的:%1。但是,也有一些标签是黑名单中的:%2。你依然想要下载吗? + 这个图片中的一些标签是白名单中的:%1。但是,也有一些标签是黑名单中的:%2。你依然想要下载吗? - - + + Page %1 of %2 (%3 of %4) - 第 %1 页 共 %2 页 (第 %3 页 共 %4 页) + 第 %1 页 共 %2 页 (第 %3 页 共 %4 页) - - - + + + max %1 - + No result - 没有结果 + 没有结果 - + Possible reasons: %1 - 可能的原因:%1 + 可能的原因:%1 - + Delete - 删除 + 删除 - + Save - 保存 + 保存 - + Save as... - 保存为... + 保存为... - + Save selected - + Save image - 保存图片 + 保存图片 - + Blacklist - 黑名单 + 黑名单 - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? - + %1 这个图片中的 %n 标签被检测到在黑名单中。你依然想要显示它吗? @@ -4005,7 +4011,7 @@ Please solve the issue before resuming the download. 选择一个日期 - + Search an image 搜索一个图片 @@ -4054,7 +4060,7 @@ Please solve the issue before resuming the download. - + Unable to guess site's type. Are you sure about the url? 无法猜测站点类型。你确定这是一个 url 吗? @@ -4127,8 +4133,8 @@ Please solve the issue before resuming the download. - - + + Name 名称 @@ -4158,17 +4164,16 @@ Please solve the issue before resuming the download. 最大同时下载数 - Images per page - 每页图片数 + 每页图片数 - + Interval (thumbnail) 时间间隔(请求缩略图) - + Interval (image) 时间间隔(请求图片) @@ -4178,126 +4183,121 @@ Please solve the issue before resuming the download. 时间间隔(请求页面) - + Interval (details) 时间间隔(请求图片详情) - + Interval (error) 时间间隔(发生错误) - - + + + + - - s s - + Sources 来源 - + Source 1 来源 1 - - - - + + + + XML XML - - - - + + + + JSON JSON - - - - + + + + Regex 正则表达式 - - - - + + + + RSS RSS - + Source 2 来源 2 - + Source 3 来源 3 - + Source 4 来源 4 - + Use default sources 使用默认来源 - Credentials - 验证 + 验证 - - + Username 用户名 - - + Password 密码 - Hash password - 计算密码哈希值 + 计算密码哈希值 - - + Test 测试 - + Login 登录 - + Type 类型 - + Through URL 通过 url @@ -4310,171 +4310,130 @@ Please solve the issue before resuming the download. 请求方式 - + GET GET - + POST POST - + OAuth 1 - + OAuth 2 - - Page limit - - - - - URL - URL - - - - Username field - - - - - Password field - + URL - - Cookie - Cookie - - - - Request token url - - - - - Authorize url - - - - - Access token url - - - - - Request url - + Cookie - - Token url - - - - - Refresh token url - - - - - Scope - - - - + Cookies Cookies - - + + Value - - + + Add 添加 - + Headers 请求头部 - + Delete 删除 - + Cancel 取消 - + Confirm 确定 - + + API key + + + + + Consumer key + + + + + Consumer secret + + + Hash a password - 计算密码哈希值 + 计算密码哈希值 - Please enter your password below.<br/>It will then be hashed using the format "%1". - 请在下面输入你的密码。<br/>它将以 "%1" 的格式计算哈希值。 + 请在下面输入你的密码。<br/>它将以 "%1" 的格式计算哈希值。 - + Delete a site 删除站点 - + Are you sure you want to delete the site %1? 你确定想要删除站点 %1 吗? - + Connection... - + Success! 测试成功! - + Failure 测试失败 - + Unable to test 无法测试 - + Error - 错误 + 错误 - + You should at least select one source @@ -4484,61 +4443,61 @@ Please solve the issue before resuming the download. Sources - 来源 + 来源 Check all - 全选 + 全选 ... - ... + ... Add - 添加 + 添加 Cancel - 取消 + 取消 Ok - 确定 + 确定 - + Options - 选项 + 选项 - + An update for this source is available. - 这个来源的设置可更新。 + 这个来源的设置可更新。 - + - No preset selected - - + Create a new preset - - + + Name - 名称 + 名称 - + Edit preset @@ -4548,47 +4507,47 @@ Please solve the issue before resuming the download. First launch - 第一次启动 + 第一次启动 Before starting, the program needs some informations to work properly. You can skip this step, and these informations will be asked later. - 在开始前,这个程序需要一些信息来开始工作。你可以跳过这些步骤,这些信息待会儿会向您询问。 + 在开始前,这个程序需要一些信息来开始工作。你可以跳过这些步骤,这些信息待会儿会向您询问。 Language - 语言 + 语言 Folder - 文件夹 + 文件夹 Browse - 浏览 + 浏览 Format - 格式 + 格式 ... - ... + ... Source - 来源 + 来源 <i>If you use Grabber for the first time, it is advised to first read the <a href="{website}/docs/">getting started</a> wiki page.</i> - + <i>如果你第一次使用 Grabber,推荐您先阅读 <a href="{website}/docs/">getting started</a> wiki 页。</i> <i>If you use Grabber for the first time, it is advised to first read the <a href="{github}/wiki/GettingStarted">getting started</a> wiki page.</i> @@ -4597,33 +4556,33 @@ Please solve the issue before resuming the download. Options - 选项 + 选项 - + Choose a save folder - + 选择保存文件夹 - + An error occurred creating the save folder. - + 创建保存目录时出错。 TagContextMenu - + Remove from favorites 从收藏移除 - + Choose as image 设置为图片 - + Add to favorites 添加到收藏 @@ -4638,47 +4597,47 @@ Please solve the issue before resuming the download. 等下再看 - + Don't blacklist - + Blacklist 黑名单 - + Don't ignore 取消忽略 - + Ignore 忽略 - + Copy tag - + Copy all tags - + Open in a new tab 在新标签页打开 - + Open in new a window 在新窗口打开 - + Open in browser 在浏览器中打开 @@ -4701,7 +4660,7 @@ Please solve the issue before resuming the download. %v - + %v @@ -4724,12 +4683,12 @@ Please solve the issue before resuming the download. 来源 - + Finished 已完成 - + %n tag(s) loaded @@ -4739,79 +4698,78 @@ Please solve the issue before resuming the download. TagTab - New tab - 新标签页 + 新标签页 - + Pl&us - Pl&us + Pl&us - + O&k - O&k + O&k - + Maybe you meant: - 或许你的意思是: + 或许你的意思是: - + Post-filtering - 后过滤 + 后过滤 - + How many sources should appear per line. - + Number of columns - 列数 + 列数 - + Images per page - 每页图片数 + 每页图片数 - + Load more results - + S&ources - &来源 + &来源 - + &Merge results - &合并结果 + &合并结果 - + Get &selected - + 获取 &已选择的图片 - + Get this &page - 获取此&页面的图片 + 获取此&页面的图片 - + Get &all - 获取&所有图片 + 获取&所有图片 - + Search - 搜索 + 搜索 @@ -4823,47 +4781,115 @@ Please solve the issue before resuming the download. - + Remove 移除 - + Add 添加 - + Kept for later 等下再看 - + Ratings 评级 - + Sortings 筛选 - + Copy 复制 - + Cut 剪切 - + Paste 粘贴 + + TokenSettingsWidget + + + Form + Form + + + + If empty + 如果为空 + + + + Separator + 分割器 + + + + Sort + + + + + Original + 源文件名 + + + + Name + 名称 + + + + If more than n tags + 如果多余 n 个标签 + + + + Keep all tags + 保留所有标签 + + + + Keep n tags + 保留 n 个标签 + + + + Keep n tags, then add + 保留 n 个标签,然后添加 + + + + Replace all tags by + 用 ... 替换所有标签 + + + + One file per tag + 每个标签只保存一个文件 + + + + Use shortest if possible + 尽量缩短 + + UpdateDialog @@ -4882,7 +4908,7 @@ Please solve the issue before resuming the download. - + Version <b>%1</b> @@ -4909,181 +4935,181 @@ Please solve the issue before resuming the download. ZoomWindow - - + + Image - 图片 + 图片 - + Save - 保存 + 保存 More details - 更多详情 + 更多详情 - + Save and close - 保存并关闭 + 保存并关闭 Destination folder - 目标文件夹 + 目标文件夹 Save as... - 保存为... + 保存为... - + Save (fav) - 保存(收藏) + 保存(收藏) - + Save and close (fav) - 保存并退出(收藏) + 保存并退出(收藏) Destination folder (fav) - 目标文件夹(收藏) + 目标文件夹(收藏) - + Reload - + Copy file - + Copy data - + Folder does not exist - 文件夹不存在 + 文件夹不存在 - + The save folder does not exist yet. Create it? - 保存文件夹不存在。创建一个吗? + 保存文件夹不存在。创建一个吗? - + Error creating folder. %1 - 创建文件夹时出错 + 创建文件夹时出错 %1 - - + + Saving... (fav) - 保存...(收藏) + 保存...(收藏) - - + + Saving... - 保存... + 保存... - + Saved! (fav) - 保存完成!(收藏) + 保存完成!(收藏) - + Saved! - 保存完成! + 保存完成! - + Copied! (fav) - 已复制!(收藏) + 已复制!(收藏) - + Copied! - 已复制! + 已复制! - + Moved! (fav) - 已移动!(收藏) + 已移动!(收藏) - + Moved! - 已移动! + 已移动! - + Link created! (fav) - + Link created! - + MD5 already exists (fav) - + MD5 already exists - + Already exists (fav) - + Already exists - + Delete (fav) - + 删除(收藏) - + Delete - 删除 + 删除 - + Close (fav) - 关闭(收藏) + 关闭(收藏) - + Close - 关闭 + 关闭 - + File is too big to be displayed. %1 @@ -5095,30 +5121,30 @@ Please solve the issue before resuming the download. %3 - - + + Error - 错误 + 错误 - + You did not specified a save folder! Do you want to open the options window? - 你没有指定保存文件夹!你想要现在打开设置窗口吗? + 你没有指定保存文件夹!你想要现在打开设置窗口吗? - + You did not specified a save format! Do you want to open the options window? - 你没有指定保存格式!你想要现在打开设置窗口吗? + 你没有指定保存格式!你想要现在打开设置窗口吗? - + Error saving image. - 保存图片时发生错误。 + 保存图片时发生错误。 - + Save image - 保存图片 + 保存图片 diff --git a/languages/English.ts b/languages/English.ts index b632b1814..2f0243f25 100644 --- a/languages/English.ts +++ b/languages/English.ts @@ -24,12 +24,12 @@ - + A new version is available: %1 - + Grabber is up to date @@ -85,47 +85,63 @@ - + Add - + Site - + Id - + Md5 - + Filename - + + <i>One ID per line.</i> + + + + + + + + + + + + <i>One MD5 per line.</i> + + + + Folder - + Browse - + Choose a save folder - + No image found. @@ -219,7 +235,7 @@ - + Pause @@ -230,45 +246,45 @@ - + Cancel - + Paused - + Resume - - + + h 'h' m 'm' s 's' - - + + m 'm' s 's' - - + + s 's' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - + Close @@ -276,17 +292,17 @@ BlacklistFix1 - + This directory does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + You are about to download information from %n image(s). Are you sure you want to continue? You are about to download information from %n image. Are you sure you want to continue? @@ -295,7 +311,7 @@ - + Blacklist fixer @@ -476,7 +492,7 @@ - + Tags @@ -502,13 +518,13 @@ - + Filename - + Folder @@ -524,163 +540,168 @@ + Galleries count as one + + + + Progress - - + + Add - + Single images - + Id - + Md5 - + Rating - + Url - + Date - + Search - + Site - + Delete all - + Delete selected - + Download - + Download selected - + Move down - + Load - + Save - + Move up - + Confirmation - + Are you sure you want to clear your download list? - + This source is not valid. - + The image per page value must be greater or equal to 1. - + The image limit must be greater or equal to 0. - + Groups (%1/%2) - - - + + + Save link list - - + + Imageboard-Grabber links (*.igl) - + Link list saved successfully! - + Error opening file. - - - + + + Load link list - + Link list loaded successfully! - + Loading %n download(s) Loading %n download @@ -688,76 +709,76 @@ - + You did not specify a save folder! - + You did not specify a filename! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Don't ask me again - + Logging in, please wait... - + Downloading pages, please wait... - + Preparing images, please wait... - + Downloading images... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + An error occured saving the image. %1 Please solve the issue before resuming the download. - + Error - - + + Getting images - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) - + %n file(s) downloaded successfully. %n file downloaded successfully. @@ -765,7 +786,7 @@ Please solve the issue before resuming the download. - + %n file(s) ignored. %n file ignored. @@ -773,7 +794,7 @@ Please solve the issue before resuming the download. - + %n file(s) already existing. %n file already existing. @@ -781,7 +802,7 @@ Please solve the issue before resuming the download. - + %n file(s) not found on the server. %n file not found on the server. @@ -789,7 +810,7 @@ Please solve the issue before resuming the download. - + %n file(s) skipped. %n file skipped. @@ -797,7 +818,15 @@ Please solve the issue before resuming the download. - + + %n file(s) skipped from a previous download. + + + + + + + %n error(s). %n error. @@ -809,7 +838,7 @@ Please solve the issue before resuming the download. EmptyDirsFix1 - + Empty folders fixer @@ -829,7 +858,7 @@ Please solve the issue before resuming the download. - + No empty folder found. @@ -837,12 +866,12 @@ Please solve the issue before resuming the download. EmptyDirsFix2 - + No folder selected. - + You are about to delete %n folder. Are you sure you want to continue? You are about to delete %n folder. Are you sure you want to continue? @@ -851,8 +880,8 @@ Please solve the issue before resuming the download. - - + + Empty folders fixer @@ -970,7 +999,7 @@ Please solve the issue before resuming the download. - + Choose an image @@ -979,7 +1008,8 @@ Please solve the issue before resuming the download. FavoritesTab - + + Favorites @@ -1034,74 +1064,74 @@ Please solve the issue before resuming the download. - + Back - + Mark as &viewed - + Get &selected - + Get this &page - + Get &all - + S&ources - + Merge results - + Mark all as vie&wed - + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - - + + No result since the %1 - - + + MM/dd/yyyy 'at' hh:mm - + Mark as viewed - + Are you sure you want to mark all your favorites as viewed? @@ -1124,12 +1154,12 @@ Please solve the issue before resuming the download. - + Warning - + You script contains error, are you sure you want to save it? @@ -1137,42 +1167,37 @@ Please solve the issue before resuming the download. GalleryTab - - New pool tab - - - - + O&k - + Post-filtering - + Number of columns - + Images per page - + Get &selected - + Get this &page - + Get &all @@ -1180,133 +1205,133 @@ Please solve the issue before resuming the download. Image - + <b>Tags:</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Rating:</b> %1<br/> - + <b>Score:</b> %1<br/> - + <b>User:</b> %1<br/><br/> - + <b>Size:</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> - + <b>Date:</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm - + <i>Unknown</i> - + yes - + no - + Tags - + ID - + MD5 - + Rating - + Score - + Author - + Date - + 'the' MM/dd/yyyy 'at' hh:mm - + Size - + Filesize - + Page - + URL - + Source(s) Source @@ -1314,37 +1339,37 @@ Please solve the issue before resuming the download. - + Sample - + Thumbnail - + Parent - + yes (#%1) - + Comments - + Children - + Notes @@ -1362,7 +1387,7 @@ Please solve the issue before resuming the download. - + Search MD5 @@ -1578,6 +1603,7 @@ Please solve the issue before resuming the download. + New tab @@ -1642,47 +1668,47 @@ Please solve the issue before resuming the download. - + No source found - + No source found. Do you have a configuration problem? Try to reinstall the program. - + &Quit - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - + Don't ask me again - + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - + Are you sure you want to quit? @@ -1736,7 +1762,7 @@ Please solve the issue before resuming the download. - + Choose a save folder @@ -1794,22 +1820,22 @@ Please solve the issue before resuming the download. - + This folder does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + Finished - + %n MD5(s) loaded %n MD5 loaded @@ -1820,12 +1846,12 @@ Please solve the issue before resuming the download. MonitoringCenter - + New images found for tag '%1' on '%2' - + %n new image(s) found for tag '%1' on '%2' @@ -1833,7 +1859,7 @@ Please solve the issue before resuming the download. - + More than %n new image(s) found for tag '%1' on '%2' @@ -1886,210 +1912,185 @@ Please solve the issue before resuming the download. - Artist tags - - - - - Copyright tags - - - - - Character tags - - - - - Species tags - - - - - Meta tags - - - - Custom token - + Interface - + Search results - + Image window - + Coloring - + Margins and borders - + Log - + Blacklist - + Monitoring - + Proxy - + Web services - - + + Commands - - + + Database - + Language - + At start - + Do nothing - + Load first page - + Restore last session - + Check for updates - + Every time - + Once a day - + Once a week - + Once a month - + Never - + Whitelist - + Download - + Don't download automatically - + When loading image - + When loading thumbnail - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> - + Ignored tags - + <i>These tags will not be taken in account when saving image.</i> - + Download images containing blacklisted tags - + Adds - - <i>These tags will be automatically added to every search.</i> + + Ask for confirmation before closing the window - - Ask for confirmation before closing the window + + Send anonymous usage data @@ -2203,7 +2204,7 @@ Please solve the issue before resuming the download. - + Favorites @@ -2331,706 +2332,649 @@ Please solve the issue before resuming the download. - - - - - If empty - - - - - - - - - Separator - - - - - - - - - If more than n tags - - - - - - - - - Keep n tags, then add - - - - - - - - - Replace all tags by - - - - - - - - - Keep n tags - - - - - - - - - Keep all tags - - - - - - - - - One file per tag - - - - - Use shortest if possible - - - - Add a custom token - + Theme - + Upscaling - + % - + Favorites display - + Image, name and details - + Image and name - + Image and details - + Name and details - + Image only - + Name only - + Details only - + Hide favorites - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> - + Source's type display - + Text - - - + + + Image - + Image and text - + Don't show - + Displayed letters - + Display n letters - + Before first dot - + Before last dot - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> - + Preload all tabs when restoring a previous session - + Use a scroll area - + Use a fixed-image-width layout - + Infinite scroll - + Disabled - + Button - + Scroll - + Remember page number when infinite scrolling - + Resize previews instead of cropping them - + Enable autocompletion - + Show warning if an incompatible modifier is found - + Show other warnings - + Download not loaded pages - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> - + Invert Click and Ctrl+Click actions - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + Tag list position - - - - + + + + Top - - - - + + + + Left - + Auto - + Preloading - + Slideshow - + s - + Middle click to close window - + Enable scroll wheel navigation - + Show tag count - + Tag order - - + + Type - + Name - + Count - + Image position - - - - - - + + + + + + Center - - - + + + Bottom - - - + + + Right - + Animation position - + Video position - + Background color - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color - + Use a single image window - + + Tags + + + + + <i>These tags and post-filters will be automatically added to every search.</i> + + + + + Post-filters + + + + + Use image samples + + + + Artists - - - - - - - - - - - - + + + + + + + + + + + + Font - + Circle - + Series - + Characters - + Models - + Generals - + Blacklisted - + Ignored - + Species - + Kept for later - + Metas - + Hosts - - + + Horizontal margins - - + + Borders - + Images - + Vertical margins - + Show log - + Blacklisted tags - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + Ignore images containing a blacklisted tag - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> - + Delay on startup - + s - + Tray icon - + Minimize to tray - + Close to tray - + Enable system tray icon - + Use proxy - + HTTP - + SOCKS v5 - - + + Host - + Port - - + + User - - + + Password - + Use system-wide proxy settings - + Add a web service - - + + Tag (after) - - + + Tag (before) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) - + Start - + End - + Credentials - + Driver - + Choose a save folder - + Choose a save folder for favorites - - + + Edit - - + + Remove - + Choose a color - + Choose a font - + An error occured creating the save folder. - + An error occured creating the favorites save folder. @@ -3038,7 +2982,7 @@ Please solve the issue before resuming the download. Page - + No valid source of the site returned result. @@ -3046,52 +2990,47 @@ Please solve the issue before resuming the download. PoolTab - - New pool tab - - - - + Pl&us - + O&k - + Maybe you meant: - + Images per page - + Number of columns - + Post-filtering - + Get &selected - + Get this &page - + Get &all @@ -3152,47 +3091,42 @@ Please solve the issue before resuming the download. QObject - - MM-dd-yyyy HH.mm - - - - + Filename must not be empty! - + Can't validate Javascript expressions. - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. - + The %%1% token does not exist and will not be replaced. - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. - + Valid filename! @@ -3202,63 +3136,63 @@ Please solve the issue before resuming the download. - + image has a "%1" token - + image does not have a "%1" token - - - + + + image's %1 does not match - - - + + + image's %1 match - - + + image is not "%1" - - + + image is "%1" - + An image needs a date to be filtered by age - + image's source does not starts with "%1" - + image's source starts with "%1" - + image does not contains "%1" - + image contains "%1" @@ -3266,17 +3200,17 @@ Please solve the issue before resuming the download. RenameExisting1 - + This folder does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + You are about to download information from %n image(s). Are you sure you want to continue? You are about to download information from %n image. Are you sure you want to continue? @@ -3284,13 +3218,13 @@ Please solve the issue before resuming the download. - + No image found when renaming image '%1' - + Rename existing images @@ -3386,105 +3320,110 @@ Please solve the issue before resuming the download. SearchTab - + + all images filtered + + + + server offline - + too many tags - + page too far - + HTTPS redirection detected - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Always - + Never for that website - + Never - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - - + + Page %1 of %2 (%3 of %4) - - - + + + max %1 - + No result - + Possible reasons: %1 - + Delete - + Save - + Save as... - + Save selected - + Save image - + Blacklist - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? @@ -3655,7 +3594,7 @@ Please solve the issue before resuming the download. - + Search an image @@ -3693,7 +3632,7 @@ Please solve the issue before resuming the download. - + Unable to guess site's type. Are you sure about the url? @@ -3766,8 +3705,8 @@ Please solve the issue before resuming the download. - - + + Name @@ -3798,16 +3737,11 @@ Please solve the issue before resuming the download. - Images per page - - - - Interval (thumbnail) - + Interval (image) @@ -3817,295 +3751,225 @@ Please solve the issue before resuming the download. - + Interval (details) - + Interval (error) - - + + + + - - s - + Sources - + Source 1 - - - - + + + + XML - - - - + + + + JSON - - - - + + + + Regex - - - - + + + + RSS - + Source 2 - + Source 3 - + Source 4 - + Use default sources - - Credentials - - - - - + Username - - + Password - - Hash password - - - - - + Test - + Login - + Type - + Through URL - + GET - + POST - + OAuth 1 - + OAuth 2 - - Username field - - - - - Password field - - - - - Page limit - - - - - - URL - - - - - - Cookie - - - - - Request token url - - - - - Authorize url - - - - - Access token url - - - - - Request url - - - - - Token url - - - - - Refresh token url - - - - - Scope - - - - + Cookies - - + + Value - - + + Add - + Headers - + Delete - + Cancel - + Confirm - - Hash a password + + API key - - Please enter your password below.<br/>It will then be hashed using the format "%1". + + Consumer key - + + Consumer secret + + + + Delete a site - + Are you sure you want to delete the site %1? - + Connection... - + Success! - + Failure - + Unable to test - + Error - + You should at least select one source @@ -4143,33 +4007,33 @@ Please solve the issue before resuming the download. - + Options - + An update for this source is available. - + - No preset selected - - + Create a new preset - - + + Name - + Edit preset @@ -4227,12 +4091,12 @@ Please solve the issue before resuming the download. - + Choose a save folder - + An error occurred creating the save folder. @@ -4240,17 +4104,17 @@ Please solve the issue before resuming the download. TagContextMenu - + Remove from favorites - + Choose as image - + Add to favorites @@ -4265,47 +4129,47 @@ Please solve the issue before resuming the download. - + Don't blacklist - + Blacklist - + Don't ignore - + Ignore - + Copy tag - + Copy all tags - + Open in a new tab - + Open in new a window - + Open in browser @@ -4343,12 +4207,12 @@ Please solve the issue before resuming the download. - + Finished - + %n tag(s) loaded @@ -4359,77 +4223,72 @@ Please solve the issue before resuming the download. TagTab - - New tab - - - - + Pl&us - + O&k - + Maybe you meant: - + Post-filtering - + How many sources should appear per line. - + Number of columns - + Images per page - + Load more results - + S&ources - + &Merge results - + Get &selected - + Get this &page - + Get &all - + Search @@ -4443,47 +4302,115 @@ Please solve the issue before resuming the download. - + Remove - + Add - + Kept for later - + Ratings - + Sortings - + Copy - + Cut - + Paste + + TokenSettingsWidget + + + Form + + + + + If empty + + + + + Separator + + + + + Sort + + + + + Original + + + + + Name + + + + + If more than n tags + + + + + Keep all tags + + + + + Keep n tags + + + + + Keep n tags, then add + + + + + Replace all tags by + + + + + One file per tag + + + + + Use shortest if possible + + + UpdateDialog @@ -4502,7 +4429,7 @@ Please solve the issue before resuming the download. - + Version <b>%1</b> @@ -4529,14 +4456,14 @@ Please solve the issue before resuming the download. ZoomWindow - - + + Image - + Save @@ -4547,7 +4474,7 @@ Please solve the issue before resuming the download. - + Save and close @@ -4563,13 +4490,13 @@ Please solve the issue before resuming the download. - + Save (fav) - + Save and close (fav) @@ -4579,157 +4506,157 @@ Please solve the issue before resuming the download. - + Reload - + Copy file - + Copy data - + Folder does not exist - + The save folder does not exist yet. Create it? - + Error creating folder. %1 - - + + Saving... (fav) - - + + Saving... - + Saved! (fav) - + Saved! - + Copied! (fav) - + Copied! - + Moved! (fav) - + Moved! - + Link created! (fav) - + Link created! - + MD5 already exists (fav) - + MD5 already exists - + Already exists (fav) - + Already exists - + Delete (fav) - + Delete - + Close (fav) - + Close - + File is too big to be displayed. %1 - - + + Error - + You did not specified a save folder! Do you want to open the options window? - + You did not specified a save format! Do you want to open the options window? - + Error saving image. - + Save image diff --git a/languages/French.ts b/languages/French.ts index f62313c62..67bf70eb3 100644 --- a/languages/French.ts +++ b/languages/French.ts @@ -28,12 +28,12 @@ Russian translation byr Николай Тихонов. - + Grabber is up to date Grabber est à jour - + A new version is available: %1 Une nouvelle version est disponible : %1 @@ -89,47 +89,63 @@ Ajouter une image - + Add Ajouter - + Site Site - + Id Id - + Md5 Md5 - + Filename Nom de fichier - + + <i>One ID per line.</i> + <i>Un ID par ligne.</i> + + + + + + + + + + + + <i>One MD5 per line.</i> + <i>Un MD5 par ligne.</i> + + + Folder Dossier - + Browse Parcourir - + Choose a save folder Choisir un dossier de sauvegarde - + No image found. Aucune image n'a été trouvée. @@ -146,149 +162,149 @@ Batch download - Téléchargement groupé + Téléchargement groupé Batch - Batch + Batch Url - Url + Url Filesize - + Taille Speed - Vitesse + Vitesse Progress - Progression + Progression Follow downloaded images - Suivre le téléchargement des images + Suivre le téléchargement des images Copy links to clipboard - Copier les liens dans le presse-papier + Copier les liens dans le presse-papier When the download is finished - À la fin du téléchargement + À la fin du téléchargement Do nothing - Ne rien faire + Ne rien faire Close window - Fermer la fenêtre + Fermer la fenêtre Open CD tray - Ouvrir le lecteur CD + Ouvrir le lecteur CD Open destination folder - Ouvrir le dossier de destination + Ouvrir le dossier de destination Play a sound - Jouer un son + Jouer un son Shutdown - Éteindre + Éteindre Remove - Retirer + Retirer Details - Détails + Détails - + Pause - Pause + Pause Skip - Passer + Passer - + Cancel - Annuler + Annuler - + Paused - En pause + En pause - + Resume - Reprendre + Reprendre - - + + h 'h' m 'm' s 's' - h 'h' m 'm' s 's' + h 'h' m 'm' s 's' - - + + m 'm' s 's' - m 'm' s 's' + m 'm' s 's' - - + + s 's' - s 's' + s 's' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - <b>Vitesse moyenne :</b> %1 %2<br/><br/><b>Temps écoulé :</b> %3<br/><b>Temps restant :</b> %4 + <b>Vitesse moyenne :</b> %1 %2<br/><br/><b>Temps écoulé :</b> %3<br/><b>Temps restant :</b> %4 - + Close - Fermer + Fermer BlacklistFix1 - + Blacklist fixer Réparateur de liste noire @@ -338,17 +354,17 @@ Annuler - + This directory does not exist. Ce dossier n'existe pas. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Si vous voulez récupérer le MD5 depuis le nom de fichier, vous devez include le token %md5% dans celui-ci. - + You are about to download information from %n image(s). Are you sure you want to continue? Vous vous apprêtez à télécharger les informations de %n image. Êtes-vous sûr de vouloir continuer ? @@ -404,37 +420,37 @@ Add a custom token - Ajouter un symbole personnalisé + Ajouter un symbole personnalisé <i>You can either use a token or tags as a condition.</i> - <i>Vous pouvez utiliser un symbole ou des tags en tant que condition.</i> + <i>Vous pouvez utiliser un symbole ou des tags en tant que condition.</i> Condition - Condition + Condition Filename - Nom de fichier + Nom de fichier Folder - Dossier + Dossier <i>Leave empty to use the default folder.</i> - <i>Laisser vide pour utiliser le dossier par défaut.</i> + <i>Laisser vide pour utiliser le dossier par défaut.</i> <i>Leave empty to use the default filename.</i> - <i>Laisser vide pour utiliser le nom de fichier par défaut.</i> + <i>Laisser vide pour utiliser le nom de fichier par défaut.</i> @@ -570,341 +586,355 @@ Downloads - Téléchargements + Téléchargements Groups (0/0) - Groupes (0/0) + Groupes (0/0) - + Tags - Tags + Tags Source - Source + Source Page - Page + Page Images per page - Images par page + Images par page Images limit - Limite d'images + Limite d'images - + Filename - Nom de fichier + Nom de fichier - + Folder - Dossier + Dossier Post-filtering - Post-filtrage + Post-filtrage Get blacklisted - Récupérer liste noire + Récupérer liste noire + Galleries count as one + Les galeries comptent pour un + + + Progress - Progression + Progression - - + + Add - Ajouter + Ajouter - + Single images - Images seules + Images seules - + Id - Id + Id - + Md5 - Md5 + Md5 - + Rating - Classe + Classe - + Url - Url + Url - + Date - Date + Date - + Search - Recherche + Recherche - + Site - Site + Site - + Delete all - Tout effacer + Tout effacer - + Delete selected - Effacer la séléction + Effacer la séléction - + Download - + Télécharger - + Download selected - Télécharger la séléction + Télécharger la séléction - + Move down - Descendre + Descendre - + Load - Charger + Charger - + Save - Enregistrer + Enregistrer - + Move up - Monter + Monter - + Confirmation - Validation + Validation - + Are you sure you want to clear your download list? - Êtes-vous sûr de vouloir vider votre liste de téléchargement ? + Êtes-vous sûr de vouloir vider votre liste de téléchargement ? - + This source is not valid. - Cette source n'est pas valide. + Cette source n'est pas valide. - + The image per page value must be greater or equal to 1. - La limite d'images par page doit être supérieure ou égale à 1. + La limite d'images par page doit être supérieure ou égale à 1. - + The image limit must be greater or equal to 0. - La limite d'images doit être supérieure ou égale à 0. + La limite d'images doit être supérieure ou égale à 0. - + Groups (%1/%2) - Groupes (%1/%2) + Groupes (%1/%2) - - - + + + Save link list - Enregistrer la liste de liens + Enregistrer la liste de liens - - + + Imageboard-Grabber links (*.igl) - Liens Imageboard-Grabber (*.igl) + Liens Imageboard-Grabber (*.igl) - + Link list saved successfully! - Liste de liens enregistrée avec succès ! + Liste de liens enregistrée avec succès ! - + Error opening file. - Erreur lors de l'ouverture du fichier. + Erreur lors de l'ouverture du fichier. - - - + + + Load link list - Charger une liste de liens + Charger une liste de liens - + Link list loaded successfully! - Liste de liens chargée avec succès ! + Liste de liens chargée avec succès ! - + Loading %n download(s) - + Chargement de %n téléchargement Chargement de %n téléchargements - + You did not specify a save folder! - Vous n'avez pas précisé de dossier de sauvegarde ! + Vous n'avez pas précisé de dossier de sauvegarde ! - + You did not specify a filename! - Vous n'avez pas précisé de nom de fichier ! + Vous n'avez pas précisé de nom de fichier ! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Vous êtes sur le point de télécharger jusqu'à %1 images, ce qui peut prendre beaucoup de temps et d'espace disque. Êtes-vous sûr de vouloir continuer ? - + Don't ask me again - + Ne pas me re-demander - + Logging in, please wait... - Connexion aux sources, veuillez patienter... + Connexion aux sources, veuillez patienter... - + Downloading pages, please wait... - Téléchargement des pages, veuillez patienter... + Téléchargement des pages, veuillez patienter... - + Preparing images, please wait... - Préparation des images, veuillez patienter... + Préparation des images, veuillez patienter... - + Downloading images... - Téléchargement des images en cours... + Téléchargement des images en cours... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + Pas assez d'espace dans le disque de destination : %1. +Veuillez libérer de l'espace avant de reprendre le téléchargement. - + An error occured saving the image. %1 Please solve the issue before resuming the download. - Une erreur est survenue lors de l'enregistrement de l'image. + Une erreur est survenue lors de l'enregistrement de l'image. %1 -Veuillez résoudre le problème avant de reprendre le téléchargement. +Veuillez résoudre le problème avant de reprendre le téléchargement. - + Error - Erreur + Erreur - - + + Getting images - Récupération des images + Récupération des images - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) - Des erreurs sont survenues pendant le téléchargement des images. Voulez vous relancer le téléchargement de celles-ci ? (%1/%2) + Des erreurs sont survenues pendant le téléchargement des images. Voulez vous relancer le téléchargement de celles-ci ? (%1/%2) - + %n file(s) downloaded successfully. - + %n fichier récupéré avec succès. %n fichiers récupérés avec succès. - + %n file(s) ignored. - + %n fichier ignoré. %n fichiers ignorés. - + %n file(s) already existing. - + %n fichier déjà existant. %n fichiers déjà existants. - + %n file(s) not found on the server. - + %n fichier non trouvé sur le serveur. %n fichiers non trouvés sur le serveur. - + %n file(s) skipped. - + %n fichier passé. %n fichiers passés. - + + %n file(s) skipped from a previous download. + + %n fichier du téléchargement repris passé. + %n fichiers du téléchargement repris passés. + + + + %n error(s). - + %n erreur. %n erreurs. @@ -914,7 +944,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. EmptyDirsFix1 - + Empty folders fixer Réparateur de dossiers vides @@ -934,7 +964,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Annuler - + No empty folder found. Aucun dossie vide trouvé. @@ -958,18 +988,18 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - - + + Empty folders fixer Réparateur de dossiers vides - + No folder selected. Aucun dossier sélectionné. - + You are about to delete %n folder. Are you sure you want to continue? Vous vous apprêtez à supprimer %n dossier. Êtes-vous sûr de vouloir continuer ? @@ -982,233 +1012,234 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Edit a favorite - Modifier un favori + Modifier un favori General - Général + Général Tag corresponding to the favorite. It is not often useful to change it. - Le tag correspond au favori. Il n'est pas souvent utile de le modifier. + Le tag correspond au favori. Il n'est pas souvent utile de le modifier. Tag - Tag + Tag Between 0 and 100, the note can be used to sort the favorites in preference order. - Comprise entre 0 et 100, la note peut être utilisée pour trier les favoris par ordre de préférence. + Comprise entre 0 et 100, la note peut être utilisée pour trier les favoris par ordre de préférence. Note - Note + Note % - % + % Last time you clicked on "Mark as viewed". - Dernière fois que vous avez cliqué sur "Marquer comme vu". + Dernière fois que vous avez cliqué sur "Marquer comme vu". Last view - Dernière vue + Dernière vue yyyy/MM/dd HH:mm:ss - dd/MM/yyyy HH:mm:ss + dd/MM/yyyy HH:mm:ss Image whose icon will be displayed in the favorites list. - Image dont l'icone sera affichée dans la liste des favoris. + Image dont l'icone sera affichée dans la liste des favoris. Image - Image + Image Browse - Parcourir + Parcourir Monitors - + Suivi Monitoring interval - + Intervalle de suivi min - + min <i>Set the interval to 0 to disable monitoring.</i> - + <i>Mettre l'intervalle à 0 pour désactiver le suivi.</i> Source - Source + Source Delete - Supprimer + Supprimer - + Choose an image - Choisir une image + Choisir une image FavoritesTab - + + Favorites - Favoris + Favoris Sort by - Trier par + Trier par Name - Nom + Nom Note - Note + Note Last view - Dernière vue + Dernière vue Ascending - Ascendant + Ascendant Descending - Descendant + Descendant O&k - O&k + O&k Number of columns - Nombre de colonnes + Nombre de colonnes Post-filtering - Post-filtrage + Post-filtrage Images per page - Images par page + Images par page - + Back - Retour + Retour - + Mark as &viewed - Marquer comme &vu + Marquer comme &vu - + Get &selected - Prendre &séléctionnés + Prendre &séléctionnés - + Get this &page - Prendre cette &page + Prendre cette &page - + Get &all - Prendre &tous + Prendre &tous - + S&ources - S&ources + S&ources - + Merge results - Fusionner les résultats + Fusionner les résultats - + Mark all as vie&wed - Marquer tous comme v&us + Marquer tous comme v&us - + MM/dd/yyyy - dd/MM/yyyy + dd/MM/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - + <b>Nom :</b> %1<br/><b>Note :</b> %2 %<br/><b>Dernière vue :</b> %3 - - + + No result since the %1 - Aucun résultat depuis le %1 + Aucun résultat depuis le %1 - - + + MM/dd/yyyy 'at' hh:mm - dd/MM/yyyy 'à' hh:mm + dd/MM/yyyy 'à' hh:mm - + Mark as viewed - Marquer comme vu + Marquer comme vu - + Are you sure you want to mark all your favorites as viewed? - Êtes-vous sûr de vouloir marquer tous vos favoris comme vus ? + Êtes-vous sûr de vouloir marquer tous vos favoris comme vus ? @@ -1229,12 +1260,12 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Nommage Javascript - + Warning Attention - + You script contains error, are you sure you want to save it? Votre script contient des erreurs, êtes-vous sûr de vouloir l'enregistrer ? @@ -1242,44 +1273,39 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. GalleryTab - - New pool tab - - - - + O&k - O&k + O&k - + Post-filtering - Post-filtrage + Post-filtrage - + Number of columns - Nombre de colonnes + Nombre de colonnes - + Images per page - Images par page + Images par page - + Get &selected - Prendre &séléctionnés + Prendre &séléctionnés - + Get this &page - Prendre cette &page + Prendre cette &page - + Get &all - Prendre &tous + Prendre &tous @@ -1369,137 +1395,137 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Déplacement depuis <a href="file:///%1">%1</a> vers <a href="file:///%2">%2</a> - + <b>Tags:</b> %1<br/><br/> <b>Tags :</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> <b>ID :</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Nom :</b> %1<br/> - + <b>Rating:</b> %1<br/> <b>Classe :</b> %1<br/> - + <b>Score:</b> %1<br/> <b>Score :</b> %1<br/> - + <b>User:</b> %1<br/><br/> <b>Auteur :</b> %1<br/><br/> - + <b>Size:</b> %1 x %2<br/> <b>Dimensions :</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> <b>Taille :</b> %1 %2<br/> - + <b>Date:</b> %1 <b>Date :</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm 'le' dd/MM/yyyy 'à' hh:mm - + <i>Unknown</i> <i>Inconnu</i> - + yes oui - + no non - + Tags Tags - + ID ID - + MD5 MD5 - + Rating Classe - + Score Score - + Author Auteur - + Date Date - + 'the' MM/dd/yyyy 'at' hh:mm 'le' dd/MM/yyyy 'à' hh:mm - + Size Dimensions - + Filesize Taille - + Page Page - + URL URL - + Source(s) - - - + + Source + Sources @@ -1507,37 +1533,37 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Source - + Sample - + Sample - + Thumbnail Miniature - + Parent Parent - + yes (#%1) oui (#%1) - + Comments Commentaires - + Children Enfants - + Notes Notes @@ -1555,7 +1581,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Services web - + Search MD5 Recherche par MD5 @@ -1565,17 +1591,17 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Log - Log + Log Clear log - Vider le log + Vider le log Open log - Ouvrir le log + Ouvrir le log @@ -1629,7 +1655,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. <i>Each time an image is saved, an external text file will be save with the same name at the same location.</i> - <i>Chaque fois qu'une image est enregristrée, un fichier texte avec le même nom sera enregistré au même emplacement.</i> + <i>Chaque fois qu'une image est enregristrée, un fichier texte avec le même nom sera enregistré au même emplacement.</i> @@ -1661,7 +1687,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Tags - Tags + Tags Source @@ -1686,7 +1712,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Folder - Dossier + Dossier Post-filtering @@ -1759,7 +1785,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Save - Enregistrer + Enregistrer Move up @@ -1780,201 +1806,202 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Help - Aide + Aide Tools - Outils + Outils View - Affichage + Affichage File - Fichier + Fichier Kept for later - Gardés pour plus tard + Gardés pour plus tard Favorites - Favoris + Favoris Name - Nom + Nom Note - Note + Note Last viewed - Dernière vue + Dernière vue Ascending - Ascendant + Ascendant Descending - Descendant + Descendant Wiki - Wiki + Wiki Destination - Destination + Destination Reset - Réinitialiser + Réinitialiser Options - Options + Options Ctrl+P - Ctrl+P + Ctrl+P Open destination folder - Ouvrir le dossier de destination + Ouvrir le dossier de destination Quit - Quitter + Quitter About Grabber - À propos de Grabber + À propos de Grabber About Qt - À propos de Qt + À propos de Qt + New tab - Nouvel onglet + Nouvel onglet Close tab - Fermer l'onglet + Fermer l'onglet Blacklist fixer - Réparateur de liste noire + Réparateur de liste noire Empty folders fixer - Réparateur de dossiers vides + Réparateur de dossiers vides New pool tab - + Nouvel onglet pool MD5 list fixer - Réparateur de MD5 + Réparateur de MD5 Open options folder - Ouvrir le dossier des options + Ouvrir le dossier des options Project website - Site web du projet + Site web du projet Report an issue - Reporter un problème + Reporter un problème Rename existing images - Renommer les images existantes + Renommer les images existantes Project GitHub - Projet GitHub + Projet GitHub Restore last closed tab - Restaurer le dernier onglet fermé + Restaurer le dernier onglet fermé Tag loader - Chargeur de tags + Chargeur de tags - + No source found - Aucune source trouvée + Aucune source trouvée - + No source found. Do you have a configuration problem? Try to reinstall the program. - Aucune source n'a été trouvée. Auriez-vous un problème de configuration ? Essayez de réinstaller. + Aucune source n'a été trouvée. Auriez-vous un problème de configuration ? Essayez de réinstaller. - + &Quit - + &Quitter - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - Il semblerait que l'application n'ait pas été arrêtée correctement lors de sa dernière utilisation. Voulez-vous restaurer votre dernière session ? + Il semblerait que l'application n'ait pas été arrêtée correctement lors de sa dernière utilisation. Voulez-vous restaurer votre dernière session ? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - L'extension pour Mozilla Firefox "Danbooru Downloader" a été détéctée sur votre système. Souhaitez-vous en importer les préférences ? + L'extension pour Mozilla Firefox "Danbooru Downloader" a été détéctée sur votre système. Souhaitez-vous en importer les préférences ? - + Don't ask me again - + Ne pas me re-demander Groups (%1/%2) @@ -2001,19 +2028,19 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. La limite d'images doit être supérieure ou égale à 0. - + MM/dd/yyyy - dd/MM/yyyy + dd/MM/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - <b>Nom :</b> %1<br/><b>Note :</b> %2 %%<br/><b>Dernière vue :</b> %3 + <b>Nom :</b> %1<br/><b>Note :</b> %2 %%<br/><b>Dernière vue :</b> %3 - + Are you sure you want to quit? - Êtes vous sûr de vouloir quitter ? + Êtes vous sûr de vouloir quitter ? Don't keep for later @@ -2137,9 +2164,9 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - + Choose a save folder - Choisir un dossier de sauvegarde + Choisir un dossier de sauvegarde @@ -2147,72 +2174,72 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Md5 list fixer - Réparateur de Md5 + Réparateur de Md5 This tool will clear your MD5 list and fill it again with the MD5 of the files found in the folder set below. - Cet outil va vider votre liste de MD5 et la remplir à nouveau avec les MD5 des fichiers trouvés dans le dossier précisé ci-dessous. + Cet outil va vider votre liste de MD5 et la remplir à nouveau avec les MD5 des fichiers trouvés dans le dossier précisé ci-dessous. Folder - Dossier + Dossier Force md5 calculation - Forcer le calcul du md5 + Forcer le calcul du md5 Get md5 in filename - Récupérer le md5 dans le nom du fichier + Récupérer le md5 dans le nom du fichier Filename - Nom de fichier + Nom de fichier %v/%m - %v/%m + %v/%m Start - + Démarrer Cancel - Annuler + Annuler Suffixes - Suffixes + Suffixes - + This folder does not exist. - Ce dossier n'existe pas. + Ce dossier n'existe pas. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - Si vous voulez récupérer le MD5 depuis le nom de fichier, vous devez include le token %md5% dans celui-ci. + Si vous voulez récupérer le MD5 depuis le nom de fichier, vous devez include le token %md5% dans celui-ci. - + Finished - Terminé + Terminé - + %n MD5(s) loaded - + %n MD5 chargé %n MD5 chargés @@ -2221,30 +2248,30 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. MonitoringCenter - + New images found for tag '%1' on '%2' - + Nouvelles images trouvées pour le tag '%1' sur '%2' - + %n new image(s) found for tag '%1' on '%2' - - - + + %n nouvelle image trouvée pour le tag '%1' sur '%2' + %n nouvelles images trouvées pour le tag '%1' sur '%2' - + More than %n new image(s) found for tag '%1' on '%2' - - - + + Plus de %n nouvelle image trouvée pour le tag '%1' sur '%2' + Plus de %n nouvelles images trouvées pour le tag '%1' sur '%2' Grabber monitoring - + Suivi de Grabber @@ -2252,281 +2279,271 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Options - Options + Options General - Général + Général Sources - Sources + Sources Save - Enregistrer + Enregistrer Filename - Nom de fichier + Nom de fichier Conditional filenames - Noms conditionnels + Noms conditionnels Separate log files - Fichiers de log séparés + Fichiers de log séparés - Artist tags - Tags artiste + Tags artiste - Copyright tags - Tags série + Tags série - Character tags - Tags personnage + Tags personnage - Species tags - Tags espèce + Tags espèce - - Meta tags - - - - + Custom token - Symbole personnalisé + Symbole personnalisé - + Interface - Interface + Interface - + Search results - Résultats de recherche + Résultats de recherche - + Image window - Fenêtre d'image + Fenêtre d'image - + Coloring - Coloration + Coloration - + Margins and borders - Marges et bordures + Marges et bordures - + Log - Log + Log - + Blacklist - + Liste noire - + Monitoring - + Suivi - + Proxy - Proxy + Proxy - + Web services - Services web + Services web - - + + Commands - Commandes + Commandes - - + + Database - Base de données + Base de données - + Language - + Language - + At start - Au démarrage + Au démarrage - + Do nothing - Ne rien faire + Ne rien faire - + Load first page - Charger la première page + Charger la première page - + Restore last session - Restaurer la dernière session + Restaurer la dernière session - + Check for updates - Vérification de mise à jour + Vérification de mise à jour - + Every time - À chaque fois + À chaque fois - + Once a day - Une fois par jour + Une fois par jour - + Once a week - Une fois par semaine + Une fois par semaine - + Once a month - Une fois par mois + Une fois par mois - + Never - Jamais + Jamais - + Whitelist - Liste blanche + Liste blanche - + Download - + Télécharger - + Don't download automatically - Ne pas télécharger automatiquement + Ne pas télécharger automatiquement - + When loading image - Au chargement de l'image + Au chargement de l'image - + When loading thumbnail - Au chargement de la miniature + Au chargement de la miniature - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> - <i>Les images contenant un tag de la liste blanche seront téléchargées automatiquement en fonction de l'option ci-dessus.</i> + <i>Les images contenant un tag de la liste blanche seront téléchargées automatiquement en fonction de l'option ci-dessus.</i> - + Ignored tags - Tags ignorés + Tags ignorés - + <i>These tags will not be taken in account when saving image.</i> - <i>Ces tags ne seront pas pris en compte lors de la sauvegarde de l'image.</i> + <i>Ces tags ne seront pas pris en compte lors de la sauvegarde de l'image.</i> - + Download images containing blacklisted tags - Télécharger les images de la liste noire + Télécharger les images de la liste noire - + Adds - Ajouts + Ajouts - <i>These tags will be automatically added to every search.</i> - <i>Ces tags seront automatiquement ajoutés à chaque recherche.</i> + <i>Ces tags seront automatiquement ajoutés à chaque recherche.</i> - + Ask for confirmation before closing the window - Demander confirmation avant la fermeture de la fenêtre + Demander confirmation avant la fermeture de la fenêtre Images per page - Images par page + Images par page Number of columns - Nombre de colonnes + Nombre de colonnes Source 1 - Source 1 + Source 1 Source 2 - Source 2 + Source 2 Source 3 - Source 3 + Source 3 Source 4 - Source 4 + Source 4 Get more precise tags when searching images - Récupérer les tags les plus précis lors de la recherche d'images + Récupérer les tags les plus précis lors de la recherche d'images @@ -2534,7 +2551,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. XML - XML + XML @@ -2542,7 +2559,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. JSON - JSON + JSON @@ -2550,7 +2567,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Regex - Regex + Regex @@ -2558,888 +2575,876 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. RSS - RSS + RSS Auto tag add - Ajout automatique de tag + Ajout automatique de tag Download original images - Télécharger les images originales + Télécharger les images originales Download sample on error - Télécharger le sample en cas d'erreur + Télécharger le sample en cas d'erreur Download images automatically - Télécharger les images automatiquement + Télécharger les images automatiquement Keep original creation date - Conserver la date de création + Conserver la date de création Get extension from file header - Récupérer l'extension dans l'en-tête du fichier + Récupérer l'extension dans l'en-tête du fichier Folder - Dossier + Dossier Browse - Parcourir + Parcourir - + Favorites - Favoris + Favoris Simultaneous downloads - Téléchargements simultanés + Téléchargements simultanés When the download is finished - À la fin du téléchargement + À la fin du téléchargement Close window - Fermer la fenêtre + Fermer la fenêtre Open CD tray - Ouvrir le lecteur CD + Ouvrir le lecteur CD Play a sound - Jouer un son + Jouer un son Shutdown - Éteindre + Éteindre If a file already exists globally - Si un fichier existe déj globalement + Si un fichier existe déj globalement Copy - Copier + Copier Move - Déplacer + Déplacer Link - + Lien Don't save - Ne pas sauvegarder + Ne pas sauvegarder <i>File's identity is based on the MD5 algorithm.</i> - <i>L'identicité des fichiers est basée sur l'algorithme MD5.</i> + <i>L'identicité des fichiers est basée sur l'algorithme MD5.</i> Automatic redownload - Retéléchargement automatique + Retéléchargement automatique Keep deleted files in the MD5 list - Conserver les fichiers supprimés dans la liste MD5 + Conserver les fichiers supprimés dans la liste MD5 If an image yields multiple files - + Si une image génère plusieurs fichiers Default - Par défaut + Par défaut Tags separator - Séparateur de tags + Séparateur de tags Replace spaces by underscores - Remplacer les espaces par des underscores + Remplacer les espaces par des underscores Replace JPEG by JPG - Remplacer les JPEG par des JPG + Remplacer les JPEG par des JPG Max length - Longeur limite + Longeur limite <i>If the filename length is greater than this number, it will be shortened. Leave it to 0 to use the default limit.</i> - <i>Si le nom de fichier est plus grand que ce nombre, il sera tronqué. Laissez à 0 pour laisser la limite par défaut.</i> + <i>Si le nom de fichier est plus grand que ce nombre, il sera tronqué. Laissez à 0 pour laisser la limite par défaut.</i> Add a conditional filename - Ajouter un nom de fichier conditionnel + Ajouter un nom de fichier conditionnel <i>Each time an image is saved, its information can be added to a separate text file for later processing or for organization purposes.</i> - <i>À chaque fois qu'une image est sauvegardée, ses informations peuvent être ajoutées dans un fichier texte pour un traitement ultérieur ou pour organiser sa collection.</i> + <i>À chaque fois qu'une image est sauvegardée, ses informations peuvent être ajoutées dans un fichier texte pour un traitement ultérieur ou pour organiser sa collection.</i> Add a separate log file - Ajouter un fichier de log séparé + Ajouter un fichier de log séparé - - - - - If empty - Si aucun + Si aucun - - - - - Separator - Séparateur + Séparateur - - - - - If more than n tags - Si plus de n tags + Si plus de n tags - - - - - Keep n tags, then add - Garder n tags, puis ajouter + Garder n tags, puis ajouter - - - - - Replace all tags by - Remplacer tous les tags par + Remplacer tous les tags par - - - - - Keep n tags - Garder n tags + Garder n tags - - - - - Keep all tags - Garder tous les tags + Garder tous les tags - - - - - One file per tag - Un fichier par tag + Un fichier par tag + + + Original + Origine - Use shortest if possible - Utiliser le plus court si possible + Utiliser le plus court si possible - + Add a custom token - Ajouter un symbole personnalisé + Ajouter un symbole personnalisé - + Theme - Thème + Thème - + Upscaling - Suréchantillonnage + Suréchantillonnage - + % - % + % - + Favorites display - Affichage des favoris + Affichage des favoris - + Image, name and details - Image, nom et détails + Image, nom et détails - + Image and name - Image et nom + Image et nom - + Image and details - Image et détails + Image et détails - + Name and details - Nom et détails + Nom et détails - + Image only - Image seulement + Image seulement - + Name only - Nom seulement + Nom seulement - + Details only - Détails seulement + Détails seulement - + Hide favorites - Cacher les favoris + Cacher les favoris - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> - <i>La liste des favoris sera cachée dès que ce nombre d'images reçues sera atteint.</i> + <i>La liste des favoris sera cachée dès que ce nombre d'images reçues sera atteint.</i> - + Source's type display - Affichage des types de source + Affichage des types de source - + Text - Texte + Texte - - - + + + Image - Image + Image - + Image and text - Image et texte + Image et texte - + Don't show - Ne pas afficher + Ne pas afficher - + Displayed letters - Lettres affichées + Lettres affichées - + Display n letters - Afficher n lettres + Afficher n lettres - + Before first dot - Avant le premier point + Avant le premier point - + Before last dot - Avant le dernier point + Avant le dernier point - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> - <i>Nombre de lettres affichées à côté des cases à cocher des sources en bas de la fenêtre principale.</i> + <i>Nombre de lettres affichées à côté des cases à cocher des sources en bas de la fenêtre principale.</i> - + Preload all tabs when restoring a previous session - Pré-charger tous les onglets en restaurant la session précédente + Pré-charger tous les onglets en restaurant la session précédente - + Use a scroll area - Utiliser une zonne scrollable + Utiliser une zonne scrollable - + Use a fixed-image-width layout - Utiliser un layout avec des images à largeur fixe + Utiliser un layout avec des images à largeur fixe - + Infinite scroll - Scroll infini + Scroll infini - + Disabled - Désactivé + Désactivé - + Button - Bouton + Bouton - + Scroll - Défilement souris + Défilement souris - + Remember page number when infinite scrolling - + Se souvenir du numéro de page lors du scroll infini - + Resize previews instead of cropping them - Redimensionner les miniatures au lieu de les couper + Redimensionner les miniatures au lieu de les couper - + Enable autocompletion - Activer l'autocomplétion + Activer l'autocomplétion - + Show warning if an incompatible modifier is found - Afficher l'avertissement en cas de modificateur incompatible + Afficher l'avertissement en cas de modificateur incompatible - + Show other warnings - Afficher les autre avertissements + Afficher les autre avertissements - + Download not loaded pages - Télécharger les pages non chargées + Télécharger les pages non chargées - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> - <i>Si vous activez cette option, appuyer sur le bouton "Télécharger cette page" prendra en compte les modifications faîtes au nombre d'images par page, numéro de page, etc. même si elles n'ont pas été chargées.</i> + <i>Si vous activez cette option, appuyer sur le bouton "Télécharger cette page" prendra en compte les modifications faîtes au nombre d'images par page, numéro de page, etc. même si elles n'ont pas été chargées.</i> - + Invert Click and Ctrl+Click actions - + Inverser les action du Click et du Ctrl+Click - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + <i>Avec cette option activée, cliquer une image la marquera pour le téléchargement, tandis que Ctrl+Click ouvrira la fenêtre de détails.</i> - + Tag list position - Position de la liste de tags + Position de la liste de tags - - - - + + + + Top - Haut + Haut - - - - + + + + Left - Gauche + Gauche - + Auto - Auto + Auto - + Preloading - Pré-chargement + Pré-chargement - + Slideshow - Diaporama + Diaporama - + s - s + s - + Middle click to close window - Clic milieu pour fermer la fenêtre + Clic milieu pour fermer la fenêtre - + Enable scroll wheel navigation - Activer la navigation à la molette + Activer la navigation à la molette - + Show tag count - Afficher le compteur de tags + Afficher le compteur de tags - + Tag order - Ordre des tags + Ordre des tags - - + + Type - Type + Type - + Name - Nom + Nom - + Count - Compte + Compte - + Image position - Position des images + Position des images - - - - - - + + + + + + Center - Centre + Centre - - - + + + Bottom - Bas + Bas - - - + + + Right - Droite + Droite - + Animation position - Position des animations + Position des animations - + Video position - Position des vidéos + Position des vidéos - + Background color - Couleur de fond + Couleur de fond - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color - Couleur + Couleur - + Use a single image window - + Utiliser une seule fenêtre d'images + + + + Tags + Tags + + + + <i>These tags and post-filters will be automatically added to every search.</i> + <i>Ces tags seront automatiquement ajoutés à chaque recherche.</i> - + + Post-filters + Post-filtrage + + + + Send anonymous usage data + Envoyer des statistiques d'utilisation anonymes + + + + Use image samples + Utiliser les images de sample + + + Artists - Artistes - - - - - - - - - - - - - - + Artistes + + + + + + + + + + + + + + Font - Fonte + Fonte - + Circle - Cercle + Cercle - + Series - Séries + Séries - + Characters - Personnages + Personnages - + Models - Modèles + Modèles - + Generals - Généraux + Généraux - + Blacklisted - Sur liste noire + Sur liste noire - + Ignored - Ignorés + Ignorés - + Species - Espèces + Espèces - + Kept for later - Gardés pour plus tard + Gardés pour plus tard - + Metas - + Metas - + Hosts - Serveurs + Serveurs - - + + Horizontal margins - Marges horizontales + Marges horizontales - - + + Borders - Bordures + Bordures - + Images - Images + Images - + Vertical margins - Marges verticales + Marges verticales - + Show log - Afficher le log + Afficher le log - + Blacklisted tags - + Liste noire - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + <i>Une ligne par liste noire. Vous pouvez mettre plusieurs tags sur une seule ligne pour faire des conditions "ET".</i> - + Ignore images containing a blacklisted tag - Ignorer les images contenant un tag de la liste noire + Ignorer les images contenant un tag de la liste noire - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> - <i>Les images contenant un tag de la liste noire ne seront tout simplement pas affichées dans la liste de résultats si cette case est cochée. Sinon, une confirmation sera demandée avant l'affichage d'une de ces images.</i> + <i>Les images contenant un tag de la liste noire ne seront tout simplement pas affichées dans la liste de résultats si cette case est cochée. Sinon, une confirmation sera demandée avant l'affichage d'une de ces images.</i> - + Delay on startup - + Délai au démarrage - + s - + s - + Tray icon - + Icône de la barre d'état - + Minimize to tray - + Minimiser dans la barre d'état - + Close to tray - + Fermer dans la barre d'état - + Enable system tray icon - + Activer l'icône de la barre d'état - + Use proxy - Utiliser un proxy + Utiliser un proxy - + HTTP - HTTP + HTTP - + SOCKS v5 - SOCKS v5 + SOCKS v5 - - + + Host - Serveur + Serveur - + Port - Port + Port - - + + User - Utilisateur + Utilisateur - - + + Password - Mot de passe + Mot de passe - + Use system-wide proxy settings - Utiliser les paramètres proxy système + Utiliser les paramètres proxy système - + Add a web service - Ajouter un service web + Ajouter un service web - - + + Tag (after) - Tag (après) + Tag (après) - - + + Tag (before) - Tag (avant) + Tag (avant) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) - Symboles additionels : <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i> : le tag<br/><i>%type%</i> : type du tag, "general", "artist", "copyright", "character", "model" ou "photo_set"<br/><i>%number%</i> : le numéro du type de tag (varie entre 0 et 6) + Symboles additionels : <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i> : le tag<br/><i>%type%</i> : type du tag, "general", "artist", "copyright", "character", "model" ou "photo_set"<br/><i>%number%</i> : le numéro du type de tag (varie entre 0 et 6) - + Start - + Démarrage - + End - Fin + Fin - + Credentials - Identifiants + Identifiants - + Driver - Driver + Driver - + Choose a save folder - Choisir un dossier de sauvegarde + Choisir un dossier de sauvegarde - + Choose a save folder for favorites - Choisir un dossier de sauvegarde pour les favoris + Choisir un dossier de sauvegarde pour les favoris - - + + Edit - Modifier + Modifier - - + + Remove - Retirer + Retirer - + Choose a color - Choisir une couleur + Choisir une couleur - + Choose a font - Choisir une fonte + Choisir une fonte - + An error occured creating the save folder. - Une erreur est survenue lors de la création du dossier de sauvegarde. + Une erreur est survenue lors de la création du dossier de sauvegarde. - + An error occured creating the favorites save folder. - Une erreur est survenue lors de la création du dossier de sauvegarde des favoris. + Une erreur est survenue lors de la création du dossier de sauvegarde des favoris. Page - + No valid source of the site returned result. Aucune source valide du site n'a retourné de résultat. @@ -3486,54 +3491,49 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. PoolTab - - New pool tab - - - - + Pl&us - Pl&us + Pl&us - + O&k - O&k + O&k - + Maybe you meant: - Peut-être avez-vous voulu dire : + Peut-être avez-vous voulu dire : - + Images per page - Images par page + Images par page - + Number of columns - Nombre de colonnes + Nombre de colonnes - + Post-filtering - Post-filtrage + Post-filtrage - + Get &selected - Prendre &séléctionnés + Prendre &séléctionnés - + Get this &page - Prendre cette &page + Prendre cette &page - + Get &all - Prendre &tous + Prendre &tous @@ -3551,7 +3551,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Unknown option '%1'. - Option inconnue '%1'. + Option inconnue '%1'. @@ -3613,51 +3613,50 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. <b>Notice :</b> %1 - MM-dd-yyyy HH.mm - dd-MM-yyyy HH.mm + dd-MM-yyyy HH.mm Error in Javascript evaluation:<br/> Erreur d'évaluation du Javascript :<br/> - + Filename must not be empty! Le nom de fichier ne doit pas être vide ! - + Can't validate Javascript expressions. Impossible de valider les expressions Javascript. - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. Votre nom de fichier ne finit pas par une extension, symbolisée par %ext% ! Vous risquez de ne pas pouvoir ouvrir vos fichiers. - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. Votre nom de fichier n'est pas unique à chaque image et une image risque d'en écraser une précédente lors de la sauvegarde ! Vous devriez utiliser le symbole %md5%, unique à chaque image, pour éviter ce désagrément. - + The %%1% token does not exist and will not be replaced. Le symbole %%1% n'existe pas et ne sera pas remplacé. - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | Votre format contient des caractères interdits sur windows ! Caractères interdits : * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. Vous avez choisi d'utiliser le symbole %id%. Sachez que celui-ci est unique pour un site choisi. Le même ID pourra identifier des images différentes en fonction du site. - + Valid filename! Format valide ! @@ -3690,67 +3689,67 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Gio - + image has a "%1" token - + l'image contient le token "%1" - + image does not have a "%1" token - + l'image ne contient pas le token "%1" unknown type "%1" (available types: "%2") type "%1" inconnu (types disponibles : "%2") - - - + + + image's %1 does not match le %1 de l'image ne correspond pas - - - + + + image's %1 match le %1 de l'image correspond - - + + image is not "%1" l'image n'est pas "%1" - - + + image is "%1" l'image est "%1" - + An image needs a date to be filtered by age - + Une image nécéssite une date pour être filtrée par age - + image's source does not starts with "%1" la source de l'image ne commence pas par "%1" - + image's source starts with "%1" la source de l'image commence par "%1" - + image does not contains "%1" l'image ne contient pas "%1" - + image contains "%1" l'image contient "%1" @@ -3759,7 +3758,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. RenameExisting1 - + Rename existing images Renommer les images existantes @@ -3814,17 +3813,17 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Suffixes - + This folder does not exist. Ce dossier n'existe pas. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Si vous voulez récupérer le MD5 depuis le nom de fichier, vous devez include le token %md5% dans celui-ci. - + You are about to download information from %n image(s). Are you sure you want to continue? Vous vous apprêtez à télécharger les informations de %n image. Êtes-vous sûr de vouloir continuer ? @@ -3832,9 +3831,9 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - + No image found when renaming image '%1' - + Aucune image trouvée en renommant '%1' @@ -3878,107 +3877,112 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. SearchTab - + + all images filtered + toutes les images ont été filtrées + + + server offline - serveur hors ligne + serveur hors ligne - + too many tags - trop de tags + trop de tags - + page too far - page trop éloignée + page trop éloignée - + HTTPS redirection detected - + Redirection HTTPS détéctée - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Une redirection HTTP vers HTTPS a été détéctée pour le site %1. Voulez vous activer SSL dessus? L'option recommandée est 'oui'. - + Always - + Toujours - + Never for that website - + Jamais pour ce site - + Never - Jamais + Jamais - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - Certains tags de l'image sont dans la liste blanche : %1. Cependant, certains dans la liste noire : %2. Voulez-vous la télécharger tout de même ? + Certains tags de l'image sont dans la liste blanche : %1. Cependant, certains dans la liste noire : %2. Voulez-vous la télécharger tout de même ? - - + + Page %1 of %2 (%3 of %4) - Page %1 de %2 (%3 sur %4) + Page %1 de %2 (%3 sur %4) - - - + + + max %1 - + max %1 - + No result - Pas de résultat + Pas de résultat - + Possible reasons: %1 - Raisons possibles : %1 + Raisons possibles : %1 - + Delete - Supprimer + Supprimer - + Save - Enregistrer + Enregistrer - + Save as... - Enregistrer sous... + Enregistrer sous... - + Save selected - Enregistrer la sélection + Enregistrer la sélection - + Save image - Enregistrer l'image + Enregistrer l'image - + Blacklist - + Liste noire - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? - + %n tag figurant dans la liste noire détécté sur cette image : %1. Voulez-vous l'afficher tout de même ? %n tags figurant dans la liste noire détéctés sur cette image : %1. Voulez-vous l'afficher tout de même ? @@ -4147,7 +4151,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Choose a date - + Search an image Search an image @@ -4204,7 +4208,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. L'url que vous avez entrée n'est pas valide. - + Unable to guess site's type. Are you sure about the url? Impossible de deviner le type du site. Êtes-vous sûr de l'url ? @@ -4264,94 +4268,93 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Referer (image) - - + + + + - - s s - + Sources Sources - + Source 1 Source 1 - - - - + + + + XML XML - - - - + + + + JSON JSON - - - - + + + + Regex Regex - - - - + + + + RSS RSS - + Source 2 Source 2 - + Source 3 Source 3 - + Source 4 Source 4 - + Type Type - + Through URL Passage dans l'url - + GET GET - + POST POST - - + Password Mot de passe @@ -4395,8 +4398,8 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - - + + Name Nom @@ -4426,17 +4429,16 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Téléchargements simultanés max - Images per page - Images par page + Images par page - + Interval (thumbnail) Intervalle (miniature) - + Interval (image) Intervalle (image) @@ -4446,44 +4448,40 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Intervalle (page) - + Interval (details) Intervalle (détails) - + Interval (error) Intervalle (erreur) - + Use default sources Utiliser les sources par défaut - Credentials - Identifiants + Identifiants - - + Username Utilisateur - Hash password - Hasher un mot de passe + Hasher un mot de passe - - + Test Tester - + Login Connexion @@ -4496,163 +4494,162 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Méthode - - URL - URL + URL - - Cookie - Cookie + Cookie - Page limit - Page max + Page max - + OAuth 1 OAuth 1 - + OAuth 2 OAuth 2 - Username field - Champ utilisateur + Champ utilisateur - Password field - Champ mot de passe + Champ mot de passe - Request token url - Url de demande de token + Url de demande de token - Authorize url - Url 'dautorisation + Url 'dautorisation - Access token url - Url de token d'accès + Url de token d'accès - Request url - Url de requête + Url de requête - Token url - Url de token + Url de token - Refresh token url - Url de màj de token + Url de màj de token - Scope - Portée + Portée - + Cookies Cookies - - + + Value Valeur - - + + Add Ajouter - + Delete Supprimer - + Cancel Annuler - + Confirm Valider - + Headers Headers - + + API key + Clé API + + + + Consumer key + + + + + Consumer secret + + + Hash a password - Hasher un mot de passe + Hasher un mot de passe - Please enter your password below.<br/>It will then be hashed using the format "%1". - Veuillez entrer votre mot de passe ci-dessous.<br/>Il sera ensuite hashé en utilisant le format "%1". + Veuillez entrer votre mot de passe ci-dessous.<br/>Il sera ensuite hashé en utilisant le format "%1". - + Delete a site Supprimer un site - + Are you sure you want to delete the site %1? Êtes-vous sûr de vouloir supprimer le site %1 ? - + Connection... Connexion... - + Success! Succès ! - + Failure Échec - + Unable to test Impossible de tester - + Error - Erreur + Erreur - + You should at least select one source - + Vous devez sélectionner au moins une source @@ -4660,63 +4657,63 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Sources - Sources + Sources Check all - Tout cocher + Tout cocher ... - ... + ... Add - Ajouter + Ajouter Cancel - Annuler + Annuler Ok - Ok + Ok - + Options - Options + Options - + An update for this source is available. - Une mise à jour de cette source est disponible. + Une mise à jour de cette source est disponible. - + - No preset selected - - + - Aucun preset sélectionné - - + Create a new preset - Créer un nouveau preset + Créer un nouveau preset - - + + Name - Nom + Nom - + Edit preset - Modifier un preset + Modifier un preset @@ -4724,47 +4721,47 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. First launch - Premier lancement + Premier lancement Before starting, the program needs some informations to work properly. You can skip this step, and these informations will be asked later. - Avant de commencer, le programme a besoin de quelques informations nécéssaires à son bon fonctionnement. Vous pouvez ignorer cette étape, et ces informations vous seront demandées en temps et en heure. + Avant de commencer, le programme a besoin de quelques informations nécéssaires à son bon fonctionnement. Vous pouvez ignorer cette étape, et ces informations vous seront demandées en temps et en heure. Language - + Langue Folder - Dossier + Dossier Browse - Parcourir + Parcourir Format - Format + Format ... - ... + ... Source - Source + Source <i>If you use Grabber for the first time, it is advised to first read the <a href="{website}/docs/">getting started</a> wiki page.</i> - + <i>Si vous utilisez Grabber pour la première fois, il est conseillé de tout d'abord lire la page <a href="{website}/docs/">getting started</a>.</i> <i>If you use Grabber for the first time, it is advised to first read the <a href="{github}/wiki/GettingStarted">getting started</a> wiki page.</i> @@ -4773,33 +4770,33 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Options - Options + Options - + Choose a save folder - Choisir un dossier de sauvegarde + Choisir un dossier de sauvegarde - + An error occurred creating the save folder. - Une erreur est survenue lors de la création du dossier de sauvegarde. + Une erreur est survenue lors de la création du dossier de sauvegarde. TagContextMenu - + Remove from favorites Retirer des favoris - + Choose as image Choisir comme image - + Add to favorites Ajouter aux favoris @@ -4814,47 +4811,47 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Garder pour plus tard - + Don't blacklist Retirer de la liste noire - + Blacklist Liste noire - + Don't ignore Ne plus ignorer - + Ignore Ignorer - + Copy tag Copier le tag - + Copy all tags Copier tous les tags - + Open in a new tab Ouvrir dans un nouvel onglet - + Open in new a window Ouvrir dans une nouvelle fenêtre - + Open in browser Ouvrir dans le navigateur @@ -4892,7 +4889,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Generate the local tag database for a given source. Afterwards, even if the source's API does not provide tag type information, Grabber can directly check it in its local tag database. - + Génère une base de données de tags locale pour une source donnée. Après, même si l'API de la source ne donne pas d'informations sur le type des tags, Grabber peut directement regarder dans sa base de données locale. @@ -4904,12 +4901,12 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Aucune API supportant la récupération des tags trouvée - + Finished Terminé - + %n tag(s) loaded %n tags chargé @@ -4920,79 +4917,78 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. TagTab - New tab - Nouvel onglet + Nouvel onglet - + Pl&us - Pl&us + Pl&us - + O&k - O&k + O&k - + Maybe you meant: - Peut-être avez-vous voulu dire : + Peut-être avez-vous voulu dire : - + Post-filtering - Post-filtrage + Post-filtrage - + How many sources should appear per line. - + Combien de sources doivent apparaître par ligne. - + Number of columns - Nombre de colonnes + Nombre de colonnes - + Images per page - Images par page + Limite d'images - + Load more results - Charger plus de résultats + Charger plus de résultats - + S&ources - S&ources + S&ources - + &Merge results - &Fusionner les résultats + &Fusionner les résultats - + Get &selected - Prendre &séléctionnés + Prendre &séléctionnés - + Get this &page - Prendre cette &page + Prendre cette &page - + Get &all - Prendre &tous + Prendre &tous - + Search - Recherche + Recherche @@ -5004,47 +5000,115 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - + Remove Retirer - + Add Ajouter - + Kept for later Gardés pour plus tard - + Ratings Classes - + Sortings Tris - + Copy Copier - + Cut Couper - + Paste Coller + + TokenSettingsWidget + + + Form + Form + + + + If empty + Si aucun + + + + Separator + Séparateur + + + + Sort + Tri + + + + Original + Original + + + + Name + Nom + + + + If more than n tags + Si plus de n tags + + + + Keep all tags + Garder tous les tags + + + + Keep n tags + Garder n tags + + + + Keep n tags, then add + Garder n tags, puis ajouter + + + + Replace all tags by + Remplacer tous les tags par + + + + One file per tag + Un fichier par tag + + + + Use shortest if possible + Utiliser le plus court si possible + + UpdateDialog @@ -5063,7 +5127,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Voir les changements - + Version <b>%1</b> Version <b>%1</b> @@ -5094,14 +5158,14 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. ZoomWindow - - + + Image Image - + Save Enregistrer @@ -5112,7 +5176,7 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - + Save and close Enregistrer et fermer @@ -5128,13 +5192,13 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - + Save (fav) Enregistrer (fav) - + Save and close (fav) Enregistrer et fermer (fav) @@ -5144,131 +5208,131 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. Dossier de destination (fav) - + Reload Recharger - + Copy file Copier le fichier - + Copy data Copier les données - + Folder does not exist Dossier inexistant - + The save folder does not exist yet. Create it? Le dossier de sauvegarde n'existe pas encore. Le créer ? - + Error creating folder. %1 Erreur lors de la création du dossier. %1 - - + + Saving... (fav) Sauvegarde... (fav) - - + + Saving... Sauvegarde... - + Saved! (fav) Enregistré! (fav) - + Saved! Enregistré! - + Copied! (fav) Copié! (fav) - + Copied! Copié! - + Moved! (fav) Déplacé! (fav) - + Moved! Déplacé! - + Link created! (fav) - + Lien créé! (fav) - + Link created! - + Lien créé ! - + MD5 already exists (fav) MD5 déjà existant (fav) - + MD5 already exists MD5 déjà existant - + Already exists (fav) Déjà existant (fav) - + Already exists Déjà existant - + Delete (fav) Supprimer (fav) - + Delete Supprimer - + Close (fav) Fermer (fav) - + Close Fermer - + File is too big to be displayed. %1 Ce fichier est trop gros pour être affiché. @@ -5281,28 +5345,28 @@ Veuillez résoudre le problème avant de reprendre le téléchargement. - - + + Error Erreur - + You did not specified a save folder! Do you want to open the options window? Vous n'avez pas précisé de dossier de sauvegarde ! Voulez-vous ouvrir les options ? - + You did not specified a save format! Do you want to open the options window? Vous n'avez pas précisé de format de sauvegarde ! Voulez-vous ouvrir les options ? - + Error saving image. Erreur lors de la sauvegarde de l'image. - + Save image Enregistrer l'image diff --git a/languages/Russian.ts b/languages/Russian.ts index 7236ad67f..9922353d1 100644 --- a/languages/Russian.ts +++ b/languages/Russian.ts @@ -28,12 +28,12 @@ Перевод на русский: Николай Тихонов. - + Grabber is up to date Установлена последняя версия - + A new version is available: %1 Доступна новая версия: %1 @@ -89,47 +89,63 @@ Добавить изображение - + Add Добавить - + Site Сайт - + Id Id - + Md5 MD5 - + Filename Имя файла - + + <i>One ID per line.</i> + + + + + + + + + + + + + <i>One MD5 per line.</i> + + + + Folder Папка - + Browse Изменить - + Choose a save folder Выберите папку для сохранения - + No image found. Изображений не найдено. @@ -146,12 +162,12 @@ Batch download - Массовая загрузка + Массовая загрузка Batch - Группа + Группа @@ -166,22 +182,22 @@ Speed - Скорость + Скорость Progress - Прогресс + Прогресс Follow downloaded images - Следовать за загружамыми изображениями + Следовать за загружамыми изображениями Copy links to clipboard - Копировать ссылки в буфер обмена + Копировать ссылки в буфер обмена @@ -191,27 +207,27 @@ Do nothing - Ничего не делать + Ничего не делать Close window - Закрыть окно + Закрыть окно Open CD tray - Открыть дисковод + Открыть дисковод Open destination folder - Открыть папку с загрузками + Открыть папку с загрузками Play a sound - Подать звуковой сигнал + Подать звуковой сигнал @@ -226,69 +242,69 @@ Details - Подробности + Подробности - + Pause - Пауза + Пауза Skip - Пропустить + Пропустить - + Cancel - Отмена + Отмена - + Paused - На паузе + На паузе - + Resume - Возобновить + Возобновить - - + + h 'h' m 'm' s 's' - ч 'ч' м 'м' с 'с' + ч 'ч' м 'м' с 'с' - - + + m 'm' s 's' - м 'м' с 'с' + м 'м' с 'с' - - + + s 's' - с 'с' + с 'с' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - <b>Средняя скорость:</b> %1 %2<br/><br/><b>Потрачено времени:</b> %3<br/><b>Осталось ждать:</b> %4 + <b>Средняя скорость:</b> %1 %2<br/><br/><b>Потрачено времени:</b> %3<br/><b>Осталось ждать:</b> %4 - + Close - Закрыть + Закрыть BlacklistFix1 - + Blacklist fixer Фиксер черного списка @@ -338,17 +354,17 @@ Отмена - + This directory does not exist. Этой папки не существует. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Если вы хотите получить MD5 из имени файла, вы должны включить в него знак % md5%. - + You are about to download information from %n image(s). Are you sure you want to continue? Вы хотите сохранить %n изображение. Продолжить? @@ -410,32 +426,32 @@ <i>You can either use a token or tags as a condition.</i> - <i>Вы можете использовать либо переменную либо тег как условие</i> + <i>Вы можете использовать либо переменную либо тег как условие</i> Condition - Условие + Условие Filename - Имя файла + Имя файла Folder - Папка + Папка <i>Leave empty to use the default folder.</i> - <i>Оставьте пустым чтобы использовать папку по умолчанию.</i> + <i>Оставьте пустым чтобы использовать папку по умолчанию.</i> <i>Leave empty to use the default filename.</i> - <i>Оставьте пустым чтобы использовать имя по умолчанию.</i> + <i>Оставьте пустым чтобы использовать имя по умолчанию.</i> @@ -571,28 +587,28 @@ Downloads - Загрузки + Загрузки Groups (0/0) - Группы (0/0) + Группы (0/0) - + Tags - Теги + Теги Source - Источник + Источник Page - Страница + Страница @@ -606,13 +622,13 @@ - + Filename Имя файла - + Folder Папка @@ -628,163 +644,168 @@ + Galleries count as one + + + + Progress Прогресс - - + + Add Добавить - + Single images Отдельные изображения - + Id Id - + Md5 MD5 - + Rating - + Url - + Date Дата - + Search Поиск - + Site Сайт - + Delete all Очистить список - + Delete selected Удалить из списка - + Download - + Download selected Скачать выбранное - + Move down Поместить ниже - + Load Загрузить - + Save - + Move up Поместить выше - + Confirmation Подтверждение - + Are you sure you want to clear your download list? Вы уверены, что хотите очистить список загрузок? - + This source is not valid. Этот источник не действителен. - + The image per page value must be greater or equal to 1. Число изображений на страницу должно быть не меньше 1. - + The image limit must be greater or equal to 0. Лимит изображений должен быть равен или больше 0. - + Groups (%1/%2) Группы (%1/%2) - - - + + + Save link list Сохранить список ссылок - - + + Imageboard-Grabber links (*.igl) Список ссылок из программы Imageboard-Grabber(*.igl) - + Link list saved successfully! Список ссылок сохранён! - + Error opening file. Ошибка при открытии файла. - - - + + + Load link list Загрузить список ссылок - + Link list loaded successfully! Список ссылок загружен! - + Loading %n download(s) Загружается %n загрузка @@ -793,53 +814,53 @@ - + You did not specify a save folder! Вы не выбрали папку для сохранения! - + You did not specify a filename! Вы не выбрали имя файла! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Don't ask me again - + Logging in, please wait... Вход в аккаунт, пожалуйста, подождите ... - + Downloading pages, please wait... Загрузка страниц, пожалуйста подождите... - + Preparing images, please wait... Подготовка изображений, пожалуйста подождите... - + Downloading images... Загрузка изображений... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + An error occured saving the image. %1 Please solve the issue before resuming the download. @@ -848,23 +869,23 @@ Please solve the issue before resuming the download. Пожалуйста, решите эту проблему перед тем как возобновить загрузку. - + Error Ошибка - - + + Getting images Получение изображений - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) Во время загрузки изображений произошли ошибки. Хотите повторить загрузку? (%1/%2) - + %n file(s) downloaded successfully. %n файл успешно загружен. @@ -873,7 +894,7 @@ Please solve the issue before resuming the download. - + %n file(s) ignored. %n файл проигнорирован. @@ -882,7 +903,7 @@ Please solve the issue before resuming the download. - + %n file(s) already existing. %n файл уже существует. @@ -891,7 +912,7 @@ Please solve the issue before resuming the download. - + %n file(s) not found on the server. %n файл не найден на сервере. @@ -900,7 +921,7 @@ Please solve the issue before resuming the download. - + %n file(s) skipped. %n файл пропущен. @@ -909,7 +930,16 @@ Please solve the issue before resuming the download. - + + %n file(s) skipped from a previous download. + + + + + + + + %n error(s). %n ошибка. @@ -922,7 +952,7 @@ Please solve the issue before resuming the download. EmptyDirsFix1 - + Empty folders fixer Фиксер пустых папок @@ -942,7 +972,7 @@ Please solve the issue before resuming the download. Отмена - + No empty folder found. Пустых папок не найдено. @@ -951,8 +981,8 @@ Please solve the issue before resuming the download. EmptyDirsFix2 - - + + Empty folders fixer Фиксер пустых папок @@ -972,12 +1002,12 @@ Please solve the issue before resuming the download. Отмена - + No folder selected. Папка не выбрана. - + You are about to delete %n folder. Are you sure you want to continue? Вы собираетесь удалить %n папку. Вы уверены что хотите продолжить? @@ -1084,7 +1114,7 @@ Please solve the issue before resuming the download. Удалить - + Choose an image Выберите изображение @@ -1093,131 +1123,132 @@ Please solve the issue before resuming the download. FavoritesTab - + + Favorites - Избранное + Избранное Sort by - Сортировать по + Сортировать по Name - + Имя Note - + Заметка Last view - + Последний просмотр Ascending - По возрастанию + По возрастанию Descending - По убыванию + По убыванию O&k - O&k + O&k Number of columns - + Количество столбцов Post-filtering - Пост-фильтр + Пост-фильтр Images per page - + Изображений на странице - + Back - Назад + Назад - + Mark as &viewed - Отметить как &просмотренное + Отметить как &просмотренное - + Get &selected - Скачать &выбранные + Скачать &выбранные - + Get this &page - Скачать всю &страницу + Скачать всю &страницу - + Get &all - Скачать &все + Скачать &все - + S&ources - И&сточники + И&сточники - + Merge results - Объединить результаты + Объединить результаты - + Mark all as vie&wed - Отметить все как п&росмотренные + Отметить все как п&росмотренные - + MM/dd/yyyy - + dd/MM/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - + <b>Название:</b> %1<br/><b>Заметка:</b> %2 %%<br/><b>Последний просмотр:</b> %3 - - + + No result since the %1 - Нет результатов с %1 + Нет результатов с %1 - - + + MM/dd/yyyy 'at' hh:mm - dd/MM/yyyy' в 'hh:mm + dd/MM/yyyy' в 'hh:mm - + Mark as viewed - Отметить как просмотренное + Отметить как просмотренное - + Are you sure you want to mark all your favorites as viewed? - Вы уверенны что хотите отметить всё избранное как просмотренное? + Вы уверенны что хотите отметить всё избранное как просмотренное? @@ -1238,12 +1269,12 @@ Please solve the issue before resuming the download. Javascript названия - + Warning Предупреждение - + You script contains error, are you sure you want to save it? Ваш скрипт содержит ошибки, вы уверены что хотите сохранить его? @@ -1251,44 +1282,43 @@ Please solve the issue before resuming the download. GalleryTab - New pool tab - Новая вкладка пула + Новая вкладка пула - + O&k - O&k + O&k - + Post-filtering - Пост-фильтр + Пост-фильтр - + Number of columns - + Количество столбцов - + Images per page - + Изображений на странице - + Get &selected - Скачать &выбранные + Скачать &выбранные - + Get this &page - Скачать всю &страницу + Скачать всю &страницу - + Get &all - Скачать &все + Скачать &все @@ -1330,133 +1360,133 @@ Please solve the issue before resuming the download. изображение содержит "%1" - + <b>Tags:</b> %1<br/><br/> <b>Теги:</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> <b>ID :</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Rating:</b> %1<br/> <b>Рейтинг:</b> %1<br/> - + <b>Score:</b> %1<br/> <b>Оценка:</b> %1<br/> - + <b>User:</b> %1<br/><br/> <b>Пользователь:</b> %1<br/><br/> - + <b>Size:</b> %1 x %2<br/> <b>Разрешение:</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> <b>Размер файла:</b> %1 %2<br/> - + <b>Date:</b> %1 <b>Дата:</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm dd/MM/yyyy' в 'hh:mm - + <i>Unknown</i> <i>Неизвестно</i> - + yes да - + no нет - + Tags Теги - + ID ID - + MD5 MD5 - + Rating Рейтинг - + Score Оценка - + Author Автор - + Date Дата - + 'the' MM/dd/yyyy 'at' hh:mm dd/MM/yyyy' в 'hh:mm - + Size Разрешение - + Filesize Размер - + Page Страница - + URL URL - + Source(s) @@ -1469,37 +1499,37 @@ Please solve the issue before resuming the download. Источник - + Sample Образец - + Thumbnail Превью - + Parent Предок - + yes (#%1) да (#%1) - + Comments Комментарии - + Children Наследник - + Notes Примечания @@ -1517,7 +1547,7 @@ Please solve the issue before resuming the download. Веб-сервисы - + Search MD5 Поиск по MD5 @@ -1527,17 +1557,17 @@ Please solve the issue before resuming the download. Log - Лог + Лог Clear log - Очистить лог + Очистить лог Open log - Открыть лог + Открыть лог @@ -1623,7 +1653,7 @@ Please solve the issue before resuming the download. Tags - Теги + Теги Source @@ -1644,7 +1674,7 @@ Please solve the issue before resuming the download. Folder - Папка + Папка Post-filtering @@ -1730,22 +1760,22 @@ Please solve the issue before resuming the download. Help - Помощь + Помощь Tools - Инструменты + Инструменты View - Просмотр + Просмотр File - Файл + Файл @@ -1758,7 +1788,7 @@ Please solve the issue before resuming the download. Favorites - Избранное + Избранное @@ -1774,72 +1804,73 @@ Please solve the issue before resuming the download. Last viewed - Последний просмотр + Последний просмотр Ascending - По возрастанию + По возрастанию Descending - По убыванию + По убыванию Wiki - Вики + Вики Destination - Папка для сохранения + Папка для сохранения Reset - Сброс + Сброс Options - Настройки + Настройки Ctrl+P - Ctrl+P + Ctrl+P Open destination folder - Открыть папку с загрузками + Открыть папку с загрузками Quit - Выход + Выход About Grabber - О программе + О программе About Qt - О платформе Qt + О платформе Qt + New tab - Новая вкладка + Новая вкладка Close tab - Закрыть вкладку + Закрыть вкладку @@ -1849,32 +1880,32 @@ Please solve the issue before resuming the download. Empty folders fixer - Фиксер пустых папок + Фиксер пустых папок New pool tab - Новая вкладка пула + Новая вкладка пула MD5 list fixer - Фиксер списка MD5 сумм + Фиксер списка MD5 сумм Open options folder - Открыть папку с настройками + Открыть папку с настройками Project website - Сайт программы + Сайт программы Report an issue - Сообщить об ошибке + Сообщить об ошибке @@ -1884,45 +1915,45 @@ Please solve the issue before resuming the download. Project GitHub - Страница на GitHub + Страница на GitHub Restore last closed tab - Восстановить закрытую вкладку + Восстановить закрытую вкладку Tag loader - Загрузчик тегов + Загрузчик тегов - + No source found - Источник не найден + Источник не найден - + No source found. Do you have a configuration problem? Try to reinstall the program. - Источник не найден. У вас проблемы с конфигурацией? Попробуйте переустановить программу. + Источник не найден. У вас проблемы с конфигурацией? Попробуйте переустановить программу. - + &Quit - + &Выход - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - Видимо приложение в прошлый раз было закрыто некорректно. Хотите ли вы восстановить сеанс? + Видимо приложение в прошлый раз было закрыто некорректно. Хотите ли вы восстановить сеанс? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - Найден плагин "Danbooru Downloader" для Mozilla Firefox. Хотите загрузить его настройки? + Найден плагин "Danbooru Downloader" для Mozilla Firefox. Хотите загрузить его настройки? - + Don't ask me again @@ -1951,19 +1982,19 @@ Please solve the issue before resuming the download. Лимит изображений должен быть равен или больше 0. - + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - <b>Название:</b> %1<br/><b>Заметка:</b> %2 %%<br/><b>Последний просмотр:</b> %3 + <b>Название:</b> %1<br/><b>Заметка:</b> %2 %%<br/><b>Последний просмотр:</b> %3 - + Are you sure you want to quit? - Вы уверены что хотите выйти? + Вы уверены что хотите выйти? Don't keep for later @@ -2094,9 +2125,9 @@ Please solve the issue before resuming the download. - + Choose a save folder - Выберите папку для сохранения + Выберите папку для сохранения @@ -2104,72 +2135,72 @@ Please solve the issue before resuming the download. Md5 list fixer - Фиксер списка MD5 + Фиксер списка MD5 This tool will clear your MD5 list and fill it again with the MD5 of the files found in the folder set below. - Этот инструмент очистит ваш список MD5 и заново заполнит его значениями MD5 файлов найденных в указанной папке. + Этот инструмент очистит ваш список MD5 и заново заполнит его значениями MD5 файлов найденных в указанной папке. Folder - Папка + Папка Force md5 calculation - + Принудительно вычислять MD5 Get md5 in filename - + Получать MD5 из имени файла Filename - Имя файла + Имя файла %v/%m - %v/%m + %v/%m Start - + Начать Cancel - Отмена + Отмена Suffixes - Суффиксы + Суффиксы - + This folder does not exist. - Этой папки не существует. + Этой папки не существует. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + Если вы хотите получить MD5 из имени файла, вы должны включить в него знак % md5%. - + Finished - Завершено + Завершено - + %n MD5(s) loaded - + %n MD5(s) загружен %n MD5(s) загружено %n MD5(s) загружено @@ -2179,12 +2210,12 @@ Please solve the issue before resuming the download. MonitoringCenter - + New images found for tag '%1' on '%2' - + %n new image(s) found for tag '%1' on '%2' @@ -2193,7 +2224,7 @@ Please solve the issue before resuming the download. - + More than %n new image(s) found for tag '%1' on '%2' @@ -2212,7 +2243,7 @@ Please solve the issue before resuming the download. Options - Настройки + Настройки @@ -2246,210 +2277,200 @@ Please solve the issue before resuming the download. Отдельные файлы лога - Artist tags - Теги художника + Теги художника - Copyright tags - Теги копирайта + Теги копирайта - Character tags - Теги персонажей + Теги персонажей - Species tags - Теги вида/расы - - - - Meta tags - + Теги вида/расы - + Custom token Свои переменные - + Interface Интерфейс - + Search results Результаты поиска - + Image window Окно превью - + Coloring Цвета - + Margins and borders Поля и границы - + Log Лог - + Blacklist - + Monitoring - + Proxy Прокси - + Web services Веб-сервисы - - + + Commands Команды - - + + Database База данных - + Language Язык - + At start При запуске программы - + Do nothing Ничего не делать - + Load first page Загрузить первую страницу - + Restore last session Восстановить последний сеанс - + Check for updates Проверять обновления - + Every time Всегда - + Once a day Раз в день - + Once a week Раз в неделю - + Once a month Раз в месяц - + Never Никогда - + Whitelist Белый список - + Download - + Don't download automatically Не скачивать автоматически - + When loading image Когда загружается изображение - + When loading thumbnail Когда загружается миниатюра - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> <i>Изображения с тегами из белого списка будут скачиваться автоматически в соответствии с этой установкой.</i> - + Ignored tags Игнорируемые теги - + <i>These tags will not be taken in account when saving image.</i> <i>Эти теги не будут приниматься во внимание при сохранении изображения.</i> - + Download images containing blacklisted tags Сохранять изображения с тегами из черного списка - + Adds Дополнительные теги - <i>These tags will be automatically added to every search.</i> - <i>Эти теги будут автоматом добавляться в каждый поисковый запрос.</i> + <i>Эти теги будут автоматом добавляться в каждый поисковый запрос.</i> - + Ask for confirmation before closing the window Спрашивать подтвеждение перед закрытием окна @@ -2564,7 +2585,7 @@ Please solve the issue before resuming the download. - + Favorites Избранное @@ -2691,707 +2712,695 @@ Please solve the issue before resuming the download. Добавить отдельный файл лога - - - - - If empty - Если пусто + Если пусто - - - - - Separator - Разделитель + Разделитель - - - - - If more than n tags - Если больше чем n тегов + Если больше чем n тегов - - - - - Keep n tags, then add - Оставить n тегов, затем добавить + Оставить n тегов, затем добавить - - - - - Replace all tags by - Заменить все теги на + Заменить все теги на - - - - - Keep n tags - Оставить n тегов + Оставить n тегов - - - - - Keep all tags - Оставить все теги + Оставить все теги - - - - - One file per tag - По одному файлу на каждый тег + По одному файлу на каждый тег + + + Original + Оригинал - Use shortest if possible - Использовать самые короткие теги если возможно + Использовать самые короткие теги если возможно - + Add a custom token - + Theme Тема - + Upscaling Масштаб - + % % - + Favorites display Отображать в избранном - + Image, name and details Изображение, название и подробности - + Image and name Изображение и название - + Image and details Изображение и подробности - + Name and details Название и подробности - + Image only Только изображение - + Name only Только название - + Details only Только подробности - + Hide favorites Скрывать избранное - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> <i>Список избранного будет скрыт как только будет достигнуто заданное количество изображений.</i> - + Source's type display Вид отоброжения источника - + Text Текст - - - + + + Image Изображение - + Image and text Изображение и текст - + Don't show Не показывать - + Displayed letters Отображаемые буквы - + Display n letters Показывать n букв - + Before first dot До первой точки - + Before last dot После последней точки - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> <i>Число отображаемых букв рядом с галочками источников в расширенном режиме поиска.</i> - + Preload all tabs when restoring a previous session Подгружать все вкладки при востановлении сессии - + Use a scroll area Использовать область прокрутки - + Use a fixed-image-width layout Использовать моноширинный вариант поисковой выдачи - + Infinite scroll Бесконечная прокрутка - + Disabled Отключена - + Button Кнопкой - + Scroll Прокруткой - + Remember page number when infinite scrolling Запоминать номер начальной страницы при бесконечной прокрутке - + Resize previews instead of cropping them Подгонять превью по размеру вместо обрезания краёв - + Enable autocompletion Включить автозавершение - + Show warning if an incompatible modifier is found Предупредить, если найдено несовместимое описание - + Show other warnings Показать остальные предупреждения - + Download not loaded pages Скачивать непрогруженные страницы - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> <i>Если активировать эту опцию, то при нажатии на кнопку "Скачать эту страницу" будут приниматься во внимание изменения, внесенные в количество изображений на страницу, количество страниц и т.д., даже если они не были загружены.</i> - + Invert Click and Ctrl+Click actions - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + Tag list position Расположение списка тегов - - - - + + + + Top Сверху - - - - + + + + Left Слева - + Auto Автоматически - + Preloading Предзагрузка - + Slideshow Слайдшоу - + s с - + Middle click to close window Зарывать окно средней клавишей мыши - + Enable scroll wheel navigation Включить навигацию колёсиком мыши - + Show tag count Показывать количество тегов - + Tag order Сортировка тегов - - + + Type Тип - + Name - + Count Количество - + Image position Расположение изображения - - - - - - + + + + + + Center По центру - - - + + + Bottom Снизу - - - + + + Right Справа - + Animation position Расположение анимации - + Video position Расположение видео - + Background color Цвет фона - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color Цвет - + Use a single image window - + + Tags + Теги + + + + <i>These tags and post-filters will be automatically added to every search.</i> + + + + + Post-filters + + + + + Send anonymous usage data + + + + + Use image samples + + + + Artists Художники - - - - - - - - - - - - + + + + + + + + + + + + Font Шрифт - + Circle Технические теги - + Series Произведения - + Characters Персонажи - + Models Модели - + Generals Общие - + Blacklisted В черном списке - + Ignored Игнорируемые - + Species Вид/Раса - + Kept for later - + Metas - + Hosts В режиме поиска по нескольким сайтам - - + + Horizontal margins Горизонтальные поля - - + + Borders Границы - + Images В режиме поиска по одному сайту / Объединённые результаты - + Vertical margins Вертикальные поля - + Show log Показывать лог - + Blacklisted tags - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + Ignore images containing a blacklisted tag Игнорировать изображения содержащие теги из черного списка - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> <i>Если включено, изображения с тегами из чёрного списка не будут отображаться в результатах, иначе каждый раз вас будут спрашивать о показе таких изображений.</i> - + Delay on startup - + s - + Tray icon - + Minimize to tray - + Close to tray - + Enable system tray icon - + Use proxy Использовать прокси - + HTTP HTTP - + SOCKS v5 SOCKS v5 - - + + Host Хост - + Port Порт - - + + User Пользователь - - + + Password Пароль - + Use system-wide proxy settings Использовать системные настройки прокси - + Add a web service Добавить веб-сервис - - + + Tag (after) Тег (перед) - - + + Tag (before) Тег (после) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) Дополнительные теги: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: означает тег<br/><i>%type%</i>:означает тип тега, "general", "artist", "copyright", "character", "model" или "photo_set"<br/><i>%number%</i>: Количество тегов этих типов (от 0 до 6) - + Start - + End Конец - + Credentials - + Driver Драйвер - + Choose a save folder Выберите папку для сохранения - + Choose a save folder for favorites Выберите папку для сохранения избранного - - + + Edit - - + + Remove - + Choose a color Выбор цвета - + Choose a font Выбор шрифта - + An error occured creating the save folder. Ошибка при создании папки для сохранения. - + An error occured creating the favorites save folder. Ошибка при создании папки для сохранения избранного. @@ -3399,7 +3408,7 @@ Please solve the issue before resuming the download. Page - + No valid source of the site returned result. Ни один из источников сайта не дал результат. @@ -3418,54 +3427,53 @@ Please solve the issue before resuming the download. PoolTab - New pool tab - Новая вкладка пула + Новая вкладка пула - + Pl&us - Пл&юс + Пл&юс - + O&k - O&k + O&k - + Maybe you meant: - Возможно вы имели в виду: + Возможно вы имели в виду: - + Images per page - + Изображений на странице - + Number of columns - + Количество столбцов - + Post-filtering - Пост-фильтр + Пост-фильтр - + Get &selected - Скачать &выбранные + Скачать &выбранные - + Get this &page - Скачать всю &страницу + Скачать всю &страницу - + Get &all - Скачать &все + Скачать &все @@ -3541,51 +3549,50 @@ Please solve the issue before resuming the download. <b>Примечание:</b> %1 - MM-dd-yyyy HH.mm - MM-dd-yyyy HH.mm + MM-dd-yyyy HH.mm Error in Javascript evaluation:<br/> Ошибка в определении Javascript: <br/> - + Filename must not be empty! Имя файла не должно быть пустым! - + Can't validate Javascript expressions. Не удается проверить выражения JavaScript. - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. Название файла не заканчивается на %ext%! Возможно, вы не сможете открыть скачанные файлы. - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. Имена файлов не уникальны, при совпадении новые изображения могут затирать старые! Вам следует использовать переменную %md5% для того чтобы избежать этого. - + The %%1% token does not exist and will not be replaced. Переменная %%1% не существует и не будет заменена. - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | Ваш формат содержит символы, запрещённые в Windows! Запрещённые символы: * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. Вы выбрали переменную %id%. Учтите, что она уникальна только внутри сайта. Под тем же ID на другом сайте может быть другое изображение. - + Valid filename! Подходящее имя файла! @@ -3618,12 +3625,12 @@ Please solve the issue before resuming the download. ГиБ - + image has a "%1" token - + image does not have a "%1" token @@ -3632,53 +3639,53 @@ Please solve the issue before resuming the download. неизвестный тип "%1" (доступные типы: "%2") - - - + + + image's %1 does not match %1 изображения не совпадает - - - + + + image's %1 match %1 изображения совпадает - - + + image is not "%1" изображение не "%1" - - + + image is "%1" изображение "%1" - + An image needs a date to be filtered by age - + image's source does not starts with "%1" источник изображения не начинается с "%1" - + image's source starts with "%1" источник изображения начинается с "%1" - + image does not contains "%1" изображение не содержит "%1" - + image contains "%1" изображение содержит "%1" @@ -3687,7 +3694,7 @@ Please solve the issue before resuming the download. RenameExisting1 - + Rename existing images Переименовать существующие изображения @@ -3742,17 +3749,17 @@ Please solve the issue before resuming the download. Суффиксы - + This folder does not exist. Этой папки не существует. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Если вы хотите получить MD5 из имени файла, вы должны включить в него знак % md5%. - + You are about to download information from %n image(s). Are you sure you want to continue? Вы хотите сохранить %n изображение. Продолжить? @@ -3761,7 +3768,7 @@ Please solve the issue before resuming the download. - + No image found when renaming image '%1' @@ -3807,105 +3814,110 @@ Please solve the issue before resuming the download. SearchTab - + + all images filtered + + + + server offline - сервер недоступен + сервер недоступен - + too many tags - слишком много тегов + слишком много тегов - + page too far - страница слишком далеко + страница слишком далеко - + HTTPS redirection detected - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Always - + Never for that website - + Never - Никогда + Никогда - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - Некоторые теги изображения находятся в белом списке: %1. Также некоторые теги находятся в черном списке: %2. Всё равно хотите сохранить изображение? + Некоторые теги изображения находятся в белом списке: %1. Также некоторые теги находятся в черном списке: %2. Всё равно хотите сохранить изображение? - - + + Page %1 of %2 (%3 of %4) - Страница %1 из %2 (%3 из %4) + Страница %1 из %2 (%3 из %4) - - - + + + max %1 - + No result - Ничего не найдено + Ничего не найдено - + Possible reasons: %1 - Возможные причины: %1 + Возможные причины: %1 - + Delete - Удалить + Удалить - + Save - + Сохранить - + Save as... - Сохранить как... + Сохранить как... - + Save selected - Сохранить выбранное + Сохранить выбранное - + Save image - Сохранить изображение + Сохранить изображение - + Blacklist - + Черный список - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? Изображение: %1 содержит тег %n из чёрного списка. Всё равно показать изображение? @@ -4077,7 +4089,7 @@ Please solve the issue before resuming the download. Выберите дату - + Search an image Искать изображение @@ -4126,7 +4138,7 @@ Please solve the issue before resuming the download. Введённый адрес недействителен. - + Unable to guess site's type. Are you sure about the url? Невозможно определить тип сайта. Вы уверенны что правильно ввели адрес? @@ -4199,8 +4211,8 @@ Please solve the issue before resuming the download. - - + + Name Имя @@ -4230,17 +4242,16 @@ Please solve the issue before resuming the download. Одновременные загрузки - Images per page - Изображений на странице + Изображений на странице - + Interval (thumbnail) Интервал (миниатюры) - + Interval (image) Интервал (изображения) @@ -4250,116 +4261,111 @@ Please solve the issue before resuming the download. Интервал (страница) - + Interval (details) Интервал (подробности) - + Interval (error) Интервал (ошибка) - - + + + + - - s с - + Sources Источники - + Source 1 Источник 1 - - - - + + + + XML XML - - - - + + + + JSON JSON - - - - + + + + Regex Regex - - - - + + + + RSS RSS - + Source 2 Источник 2 - + Source 3 Источник 3 - + Source 4 Источник 4 - + Use default sources Использовать источники по умолчанию - Credentials - Авторизация + Авторизация - - + Username Имя пользовалеля - - + Password Пароль - Hash password - Хеш пароля + Хеш пароля - - + Test Тест - + Login Войти @@ -4372,181 +4378,144 @@ Please solve the issue before resuming the download. Метод - + GET GET - + POST POST - - URL - URL + URL - - Cookie - Куки + Куки - Page limit - Лимит страниц + Лимит страниц - + Type Тип - + Through URL Через url - + OAuth 1 OAuth 1 - + OAuth 2 OAuth 2 - - Username field - - - - - Password field - - - - - Request token url - - - - - Authorize url - - - - - Access token url - - - - - Request url - - - - - Token url - - - - - Refresh token url - - - - - Scope - - - - + Cookies Куки - - + + Value Значение - - + + Add Добавить - + Headers Заголовки - + Delete Удалить - + Cancel Отмена - + Confirm Применить - + + API key + + + + + Consumer key + + + + + Consumer secret + + + Hash a password - Хешировать пароль + Хешировать пароль - Please enter your password below.<br/>It will then be hashed using the format "%1". - Пожалуйста введите ваш пароль ниже.<br/>Он будет захеширован в формате "%1". + Пожалуйста введите ваш пароль ниже.<br/>Он будет захеширован в формате "%1". - + Delete a site Удалить сайт - + Are you sure you want to delete the site %1? Вы уверенны что хотите удалить %1? - + Connection... - + Success! Успешно! - + Failure Неудача - + Unable to test Не получилось протестировать - + Error - Ошибка + Ошибка - + You should at least select one source @@ -4556,63 +4525,63 @@ Please solve the issue before resuming the download. Sources - Источники + Источники Check all - Выбрать все + Выбрать все ... - ... + ... Add - Добавить + Добавить Cancel - Отмена + Отмена Ok - Ок + Ок - + Options - Настройки + Настройки - + An update for this source is available. - Для этого источника доступно обновление. + Для этого источника доступно обновление. - + - No preset selected - - + Create a new preset - Создать шаблон + Создать шаблон - - + + Name - + Имя - + Edit preset - Изменить шаблон + Изменить шаблон @@ -4620,47 +4589,47 @@ Please solve the issue before resuming the download. First launch - Первый запуск + Первый запуск Before starting, the program needs some informations to work properly. You can skip this step, and these informations will be asked later. - Перед началом работы необходимо ввести данные для правильной работы программы. Вы можете пропустить этот шаг, и ввести эти сведения позднее. + Перед началом работы необходимо ввести данные для правильной работы программы. Вы можете пропустить этот шаг, и ввести эти сведения позднее. Language - Язык + Язык Folder - Папка + Папка Browse - + Изменить Format - Формат + Формат ... - ... + ... Source - Источник + Источник <i>If you use Grabber for the first time, it is advised to first read the <a href="{website}/docs/">getting started</a> wiki page.</i> - + <i>Если вы используете Граббер в первый раз, советуем ознакомиться с <a href="{website}/docs/">информацией для начинающих</a>(на английском).</i> <i>If you use Grabber for the first time, it is advised to first read the <a href="{github}/wiki/GettingStarted">getting started</a> wiki page.</i> @@ -4669,33 +4638,33 @@ Please solve the issue before resuming the download. Options - Настройки + Настройки - + Choose a save folder - Выберите папку для сохранения + Выберите папку для сохранения - + An error occurred creating the save folder. - Ошибка при создании папки для сохранения. + Ошибка при создании папки для сохранения. TagContextMenu - + Remove from favorites Убрать из избранного - + Choose as image Выберите как изображение - + Add to favorites Добавить в избранное @@ -4710,47 +4679,47 @@ Please solve the issue before resuming the download. Оставить на потом - + Don't blacklist Убрать из чёрного списка - + Blacklist Черный список - + Don't ignore Не игнорировать - + Ignore Игнорировать - + Copy tag Копировать тег - + Copy all tags Копировать все теги - + Open in a new tab Открыть в новой вкладе - + Open in new a window Открыть в новом окне - + Open in browser Открыть в браузере @@ -4796,12 +4765,12 @@ Please solve the issue before resuming the download. Источник - + Finished Завершено - + %n tag(s) loaded %n тег загружен @@ -4813,79 +4782,78 @@ Please solve the issue before resuming the download. TagTab - New tab - Новая вкладка + Новая вкладка - + Pl&us - Пл&юс + Пл&юс - + O&k - O&k + O&k - + Maybe you meant: - Возможно вы имели в виду: + Возможно вы имели в виду: - + Post-filtering - Пост-фильтр + Пост-фильтр - + How many sources should appear per line. - + Number of columns - + Количество столбцов - + Images per page - + Изображений на странице - + Load more results - Показать ещё результаты + Показать ещё результаты - + S&ources - И&сточники + И&сточники - + &Merge results - &Объединить результаты + &Объединить результаты - + Get &selected - Скачать &выбранные + Скачать &выбранные - + Get this &page - Скачать всю &страницу + Скачать всю &страницу - + Get &all - Скачать &все + Скачать &все - + Search - Поиск + Поиск @@ -4897,47 +4865,115 @@ Please solve the issue before resuming the download. - + Remove Убрать - + Add Добавить - + Kept for later На потом - + Ratings Рейтинги - + Sortings Сортировки - + Copy Копировать - + Cut Вырезать - + Paste Вставить + + TokenSettingsWidget + + + Form + Form + + + + If empty + Если пусто + + + + Separator + Разделитель + + + + Sort + + + + + Original + Оригинал + + + + Name + Имя + + + + If more than n tags + Если больше чем n тегов + + + + Keep all tags + Оставить все теги + + + + Keep n tags + Оставить n тегов + + + + Keep n tags, then add + Оставить n тегов, затем добавить + + + + Replace all tags by + Заменить все теги на + + + + One file per tag + По одному файлу на каждый тег + + + + Use shortest if possible + Использовать самые короткие теги если возможно + + UpdateDialog @@ -4956,7 +4992,7 @@ Please solve the issue before resuming the download. Список изменений - + Version <b>%1</b> Версия <b>%1</b> @@ -4987,14 +5023,14 @@ Please solve the issue before resuming the download. ZoomWindow - - + + Image Изображение - + Save Сохранить @@ -5005,7 +5041,7 @@ Please solve the issue before resuming the download. - + Save and close Сохранить и выйти @@ -5021,13 +5057,13 @@ Please solve the issue before resuming the download. - + Save (fav) Сохранить (избранное) - + Save and close (fav) Сохранить и закрыть (избранное) @@ -5037,131 +5073,131 @@ Please solve the issue before resuming the download. Папка для сохранения (избранное) - + Reload - + Copy file Копировать файл - + Copy data Копировать данные - + Folder does not exist Папки не существует - + The save folder does not exist yet. Create it? Папки для сохранения не существует. Создать её? - + Error creating folder. %1 Ошибка при создании папки. %1 - - + + Saving... (fav) Сохранение... (избранное) - - + + Saving... Сохранение... - + Saved! (fav) Сохранено! (избранное) - + Saved! Сохранено! - + Copied! (fav) Скопировано! (Избранное) - + Copied! Скопировано! - + Moved! (fav) Перемещено! (Избранное) - + Moved! Перемещено! - + Link created! (fav) - + Link created! - + MD5 already exists (fav) Совпадение MD5(избр) - + MD5 already exists Совпадение MD5 - + Already exists (fav) Уже существует(избр) - + Already exists Уже существует - + Delete (fav) Удалить (избр.) - + Delete Удалить - + Close (fav) Закрыть (избранное) - + Close Закрыть - + File is too big to be displayed. %1 Файл слишколь большой для предпоказа. @@ -5174,28 +5210,28 @@ Please solve the issue before resuming the download. %3 - - + + Error Ошибка - + You did not specified a save folder! Do you want to open the options window? Вы не выбрали папку для сохранения! Хотите открыть окно настроек? - + You did not specified a save format! Do you want to open the options window? Вы не выбрали формат для сохранения! Хотите открыть окно настроек? - + Error saving image. Ошибка сохранения изображения. - + Save image Сохранить изображение diff --git a/languages/Spanish.ts b/languages/Spanish.ts index e2da9ee58..b985cab50 100644 --- a/languages/Spanish.ts +++ b/languages/Spanish.ts @@ -28,12 +28,12 @@ Traducción al español por Eddy Castillo. - + Grabber is up to date Grabber está actualizado - + A new version is available: %1 Hay una nueva versión disponible: %1 @@ -89,47 +89,63 @@ Añadir imagen - + Add Añadir - + Site Sitio web - + Id Id - + Md5 MD5 - + Filename Nombre del archivo - + + <i>One ID per line.</i> + <i>Una ID por linea.</i> + + + + + + + + + + + + <i>One MD5 per line.</i> + <i>Una MD5 por linea.</i> + + + Folder Carpeta - + Browse Navegar - + Choose a save folder Elija una carpeta para guardar - + No image found. No se encontró ninguna imagen. @@ -146,149 +162,149 @@ Batch download - Descarga en lotes + Descarga en lotes Batch - Lote + Lote Url - URL + URL Filesize - + Tamaño de la Carpeta Speed - Velocidad + Velocidad Progress - Progreso + Progreso Follow downloaded images - Seguir las imágenes descargadas + Seguir las imágenes descargadas Copy links to clipboard - Copiar enlaces al portapapeles + Copiar enlaces al portapapeles When the download is finished - Al terminar la descarga + Al terminar la descarga Do nothing - No hacer nada + No hacer nada Close window - Cerrar la ventana + Cerrar la ventana Open CD tray - Abrir la bandeja de CD + Abrir la bandeja de CD Open destination folder - Abrir carpeta de destino + Abrir carpeta de destino Play a sound - Reproducir un sonido + Reproducir un sonido Shutdown - Apagar + Apagar Remove - Eliminar + Eliminar Details - Detalles + Detalles - + Pause - Pausa + Pausa Skip - Ignorar + Ignorar - + Cancel - Cancelar + Cancelar - + Paused - Pausado + Pausado - + Resume - Continuar + Continuar - - + + h 'h' m 'm' s 's' - h 'h' m 'm' s 's' + h 'h' m 'm' s 's' - - + + m 'm' s 's' - m 'm' s 's' + m 'm' s 's' - - + + s 's' - s 's' + s 's' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - <b>Velocidad promedio:</b> %1 %2<br/><br/><b>Tiempo transcurrido:</b> %3<br/><b>Tiempo faltante:</b> %4 + <b>Velocidad promedio:</b> %1 %2<br/><br/><b>Tiempo transcurrido:</b> %3<br/><b>Tiempo faltante:</b> %4 - + Close - Cerrar + Cerrar BlacklistFix1 - + Blacklist fixer Arreglar lista negra @@ -338,17 +354,17 @@ Cancelar - + This directory does not exist. Esta carpeta no existe. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Si quiere usar el MD5 en el nombre del archivo, debe incluir el identificador %md5% en él. - + You are about to download information from %n image(s). Are you sure you want to continue? Está a punto de descargar información de %n imagen. ¿Desea continuar? @@ -404,37 +420,37 @@ Add a custom token - Añadir un identificador personalizado + Añadir un identificador personalizado <i>You can either use a token or tags as a condition.</i> - <i>Puede añadir un identificador personalizado o etiquetas como condición.</i> + <i>Puede añadir un identificador personalizado o etiquetas como condición.</i> Condition - Condición + Condición Filename - Nombre del archivo + Nombre del archivo Folder - Carpeta + Carpeta <i>Leave empty to use the default folder.</i> - <i>Deje vacío el espacio para usar la carpeta predeterminada</i> + <i>Deje vacío el espacio para usar la carpeta predeterminada</i> <i>Leave empty to use the default filename.</i> - <i>Deje el espacio vacío para usar el nombre de archivo predeterminado.</i> + <i>Deje el espacio vacío para usar el nombre de archivo predeterminado.</i> @@ -570,33 +586,33 @@ Downloads - Descargas + Descargas Groups (0/0) - Grupos (0/0) + Grupos (0/0) - + Tags - Etiquetas + Etiquetas Source - Fuente + Fuente Page - Página + Página Images per page - Imágenes por página + Imágenes por página @@ -605,306 +621,319 @@ - + Filename - Nombre del archivo + Nombre del archivo - + Folder - Carpeta + Carpeta Post-filtering - Después del filtro + Después del filtro Get blacklisted - Añadir a la lista negra + Añadir a la lista negra + Galleries count as one + + + + Progress - Progreso + Progreso - - + + Add - Añadir + Añadir - + Single images - Imágenes individuales + Imágenes individuales - + Id - Id + Id - + Md5 - MD5 + MD5 - + Rating - Clasificación + Clasificación - + Url - URL + URL - + Date - Fecha + Fecha - + Search - Buscar + Buscar - + Site - Sitio web + Sitio web - + Delete all - Eliminar todo + Eliminar todo - + Delete selected - Eliminar seleccionado + Eliminar seleccionado - + Download - + Download selected - Descargar seleccionado + Descargar seleccionado - + Move down - Mover hacia abajo + Mover hacia abajo - + Load - Cargar + Cargar - + Save - Guardar + Guardar - + Move up - Mover hacia arriba + Mover hacia arriba - + Confirmation - + Are you sure you want to clear your download list? - + This source is not valid. - Esta fuente no es válida. + Esta fuente no es válida. - + The image per page value must be greater or equal to 1. - El valor de imágenes por página debe ser mayor o igual que 1. + El valor de imágenes por página debe ser mayor o igual que 1. - + The image limit must be greater or equal to 0. - El límite de imágenes debe ser mayor o igual que 0. + El límite de imágenes debe ser mayor o igual que 0. - + Groups (%1/%2) - Grupos (%1/%2) + Grupos (%1/%2) - - - + + + Save link list - Guardar lista de enlaces + Guardar lista de enlaces - - + + Imageboard-Grabber links (*.igl) - Enlaces en formato Imageboard-Grabber (*.igl) + Enlaces en formato Imageboard-Grabber (*.igl) - + Link list saved successfully! - ¡Lista de enlaces guardada correctamente! + ¡Lista de enlaces guardada correctamente! - + Error opening file. - Error al abrir el archivo. + Error al abrir el archivo. - - - + + + Load link list - Cargar lista de enlaces + Cargar lista de enlaces - + Link list loaded successfully! - ¡Lista de enlaces cargada correctamente! + ¡Lista de enlaces cargada correctamente! - + Loading %n download(s) - + Cargando %n descarga Cargando %n descargas - + You did not specify a save folder! - ¡No se especificó una carpeta para guardar! + ¡No se especificó una carpeta para guardar! - + You did not specify a filename! - ¡No se especificó un nombre de archivo! + ¡No se especificó un nombre de archivo! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Don't ask me again - + Logging in, please wait... - Iniciando sesión, por favor espere... + Iniciando sesión, por favor espere... - + Downloading pages, please wait... - Descargando páginas, por favor espere... + Descargando páginas, por favor espere... - + Preparing images, please wait... - Preparando las imágenes, por favor espere... + Preparando las imágenes, por favor espere... - + Downloading images... - Descargando imágenes... + Descargando imágenes... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + An error occured saving the image. %1 Please solve the issue before resuming the download. - Ha ocurrido un error al guardar la imagen. + Ha ocurrido un error al guardar la imagen. %1 Por favor, resuelva este problema antes de continuar las descarga. - + Error - Error + Error - - + + Getting images - Obteniendo las imágenes + Obteniendo las imágenes - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) - Ocurrieron errores durante la descarga de las imágenes. ¿Desea reiniciar la descarga de esas imágenes? (%1/%2) + Ocurrieron errores durante la descarga de las imágenes. ¿Desea reiniciar la descarga de esas imágenes? (%1/%2) - + %n file(s) downloaded successfully. - + %n archivo descargado correctamente. %n archivos descargados correctamente. - + %n file(s) ignored. - + %n archivo ignorado. %n archivos ignorados. - + %n file(s) already existing. - + %n archivo ya existe. %n archivos ya existen. - + %n file(s) not found on the server. - + %n archivo no se encontró en el servidor. %n archivos no se encontraron en el servidor. - + %n file(s) skipped. - + %n archivo ignorado. %n archivos ignorados. - - %n error(s). + + %n file(s) skipped from a previous download. + + + + + + + %n error(s). + %n error. %n errores. @@ -914,7 +943,7 @@ Por favor, resuelva este problema antes de continuar las descarga. EmptyDirsFix1 - + Empty folders fixer Arreglar carpetas vacías @@ -934,7 +963,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Cancelar - + No empty folder found. No se encontraron carpetas vacías. @@ -943,8 +972,8 @@ Por favor, resuelva este problema antes de continuar las descarga. EmptyDirsFix2 - - + + Empty folders fixer Arreglar carpetas vacías @@ -964,12 +993,12 @@ Por favor, resuelva este problema antes de continuar las descarga. Cancelar - + No folder selected. Ninguna carpeta seleccionada. - + You are about to delete %n folder. Are you sure you want to continue? Está a punto de borrar %n carpeta. ¿Desea continuar? @@ -982,67 +1011,67 @@ Por favor, resuelva este problema antes de continuar las descarga. Edit a favorite - Editar favorito + Editar favorito General - General + General Tag corresponding to the favorite. It is not often useful to change it. - Etiqueta correspondiente al favorito. Usualmente no sirve cambiarlo. + Etiqueta correspondiente al favorito. Usualmente no sirve cambiarlo. Tag - Etiqueta + Etiqueta Between 0 and 100, the note can be used to sort the favorites in preference order. - Entre 0 y 100, la nota se puede utilizar para ordenar los favoritos en orden de preferencia. + Entre 0 y 100, la nota se puede utilizar para ordenar los favoritos en orden de preferencia. Note - Nota + Nota % - % + % Last time you clicked on "Mark as viewed". - La última vez que dio clic sobre "Marcar como visto". + La última vez que dio clic sobre "Marcar como visto". Last view - Última vista + Última vista yyyy/MM/dd HH:mm:ss - yyyy/MM/dd HH:mm:ss + yyyy/MM/dd HH:mm:ss Image whose icon will be displayed in the favorites list. - Imagen cuyo ícono se mostrará en la lista de favoritos. + Imagen cuyo ícono se mostrará en la lista de favoritos. Image - Imagen + Imagen Browse - Navegar + Navegar @@ -1057,158 +1086,159 @@ Por favor, resuelva este problema antes de continuar las descarga. min - + minimo <i>Set the interval to 0 to disable monitoring.</i> - + <i>Establecer el intervalo a 0 y dejar de monitorizar. </i> Source - Fuente + Fuente Delete - Eliminar + Eliminar - + Choose an image - Elegir una imagen + Elegir una imagen FavoritesTab - + + Favorites - Favoritos + Favoritos Sort by - Ordenar por + Ordenar por Name - Nombre + Nombre Note - Nota + Nota Last view - Última vista + Última vista Ascending - Ascendente + Ascendente Descending - Descendente + Descendente O&k - Acepta&r + Acepta&r Number of columns - Número de columnas + Número de columnas Post-filtering - Después del filtro + Después del filtro Images per page - Imágenes por página + Imágenes por página - + Back - Atrás + Atrás - + Mark as &viewed - Marcar como &visto + Marcar como &visto - + Get &selected - Obtener &seleccionado + Obtener &seleccionado - + Get this &page - + Obtener esta &pagina - + Get &all - Obtener &todo + Obtener &todo - + S&ources - &Fuentes + &Fuentes - + Merge results - Combinar resultados + Combinar resultados - + Mark all as vie&wed - Marcar &todo como visto + Marcar &todo como visto - + MM/dd/yyyy - MM/dd/yyyy + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - + <b>Nombre:</b> %1<br/><b>Nota:</b> %2 %<br/><b>Ultima vista:</b> %3 - - + + No result since the %1 - Ningún resultado desde %1 + Ningún resultado desde %1 - - + + MM/dd/yyyy 'at' hh:mm - MM/dd/yyyy '-' hh:mm + MM/dd/yyyy '-' hh:mm - + Mark as viewed - Marcar como visto + Marcar como visto - + Are you sure you want to mark all your favorites as viewed? - ¿Seguro que quiere marcar todos sus favoritos como vistos? + ¿Estás seguro que quiere marcar todos sus favoritos como vistos? @@ -1229,12 +1259,12 @@ Por favor, resuelva este problema antes de continuar las descarga. Renombrado por javascript - + Warning Advertencia - + You script contains error, are you sure you want to save it? Su script contiene errores, ¿desea continuar? @@ -1242,44 +1272,43 @@ Por favor, resuelva este problema antes de continuar las descarga. GalleryTab - New pool tab - Nueva pestaña de colecciones + Nueva pestaña de colecciones - + O&k - Acepta&r + Acepta&r - + Post-filtering - Después del filtro + Después del filtro - + Number of columns - Número de columnas + Número de columnas - + Images per page - Imágenes por página + Imágenes por página - + Get &selected - Obtener &seleccionado + Obtener &seleccionado - + Get this &page - + Obtener esta &página - + Get &all - Obtener &todo + Obtener &todo @@ -1321,137 +1350,137 @@ Por favor, resuelva este problema antes de continuar las descarga. la imagen contiene "%1" - + <b>Tags:</b> %1<br/><br/> <b>Etiquetas:</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> <b>ID:</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Nombre:</b> %1<br/> - + <b>Rating:</b> %1<br/> <b>Clasificación:</b> %1<br/> - + <b>Score:</b> %1<br/> <b>Puntaje:</b> %1<br/> - + <b>User:</b> %1<br/><br/> <b>Usuario:</b> %1<br/> - + <b>Size:</b> %1 x %2<br/> <b>Dimensiones:</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> <b>Tamaño:</b> %1 %2<br/> - + <b>Date:</b> %1 <b>Fecha:</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm 'el 'MM/dd/yyyy' - 'hh:mm - + <i>Unknown</i> <i>Desconocido</i> - + yes - + no no - + Tags Etiquetas - + ID ID - + MD5 MD5 - + Rating Clasificación - + Score Puntaje - + Author Autor - + Date Fecha - + 'the' MM/dd/yyyy 'at' hh:mm 'el' MM/dd/yyyy '-' hh:mm - + Size Dimensiones - + Filesize Tamaño - + Page Página - + URL URL - + Source(s) - - - + + Fuente + Fuentes @@ -1459,37 +1488,37 @@ Por favor, resuelva este problema antes de continuar las descarga. Fuente - + Sample Muestra - + Thumbnail Miniatura - + Parent Padre - + yes (#%1) sí (#%1) - + Comments Comentarios - + Children Hijo - + Notes Notas @@ -1504,10 +1533,10 @@ Por favor, resuelva este problema antes de continuar las descarga. Web services - + Servicios web - + Search MD5 Buscar un MD5 @@ -1517,17 +1546,17 @@ Por favor, resuelva este problema antes de continuar las descarga. Log - Registros + Registros Clear log - Limpiar registros + Limpiar registros Open log - Abrir registros + Abrir registros @@ -1540,23 +1569,23 @@ Por favor, resuelva este problema antes de continuar las descarga. Location type - + Tipo de registros Path and filename - + Nombre del archivo y ruta Unique file - + Carpeta única Suffix - + Sufijo @@ -1571,7 +1600,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Path - + Ruta @@ -1581,7 +1610,7 @@ Por favor, resuelva este problema antes de continuar las descarga. <i>Each time an image is saved, an external text file will be save with the same name at the same location.</i> - + <i>Cada vez que se guarde una imagen, se creará un archivo de texto adicional con el mismo nombre en el mismo lugar, conteniendo las etiquetas de la imagen.</i> @@ -1613,7 +1642,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Tags - Etiquetas + Etiquetas Source @@ -1634,7 +1663,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Folder - Carpeta + Carpeta Post-filtering @@ -1707,7 +1736,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Save - Guardar + Guardar Move up @@ -1728,22 +1757,22 @@ Por favor, resuelva este problema antes de continuar las descarga. Help - Ayuda + Ayuda Tools - Herramientas + Herramientas View - Ver + Ver File - Archivo + Archivo @@ -1756,133 +1785,134 @@ Por favor, resuelva este problema antes de continuar las descarga. Favorites - Favoritos + Favoritos Name - Nombre + Nombre Note - Nota + Nota Last viewed - Último visto + Último visto Ascending - Ascendente + Ascendente Descending - Descendente + Descendente Wiki - Wiki + Wiki Destination - Destino + Destino Reset - Reiniciar + Reiniciar Options - Opciones + Opciones Ctrl+P - Ctrl+P + Ctrl+P Open destination folder - Abrir carpeta de destino + Abrir carpeta de destino Quit - Cerrar + Cerrar About Grabber - Acerca de Grabber + Acerca de Grabber About Qt - Acerca de Qt + Acerca de Qt + New tab - Nueva pestaña + Nueva pestaña Close tab - Cerrar pestaña + Cerrar pestaña Blacklist fixer - Arreglar lista negra + Arreglar lista negra Empty folders fixer - Arreglar carpetas vacías + Arreglar carpetas vacías New pool tab - Nueva pestaña de colecciones + Nueva pestaña de colecciones MD5 list fixer - Arreglar lista MD5 + Arreglar lista MD5 Open options folder - Abrir carpeta de opciones + Abrir carpeta de opciones Project website - Sitio web del proyecto + Sitio web del proyecto Report an issue - Reporta un problema + Reporta un problema Rename existing images - Renombrar imágenes existentes + Renombrar imágenes existentes Project GitHub - Github del proyecto + Github del proyecto @@ -1892,35 +1922,35 @@ Por favor, resuelva este problema antes de continuar las descarga. Tag loader - + Cargador de etiquetas - + No source found - No se encontró ninguna fuente + No se encontró ninguna fuente - + No source found. Do you have a configuration problem? Try to reinstall the program. - No se encontró ninguna fuente. ¿Será algún problema de configuración? Intente reinstalando el programa. + No se encontró ninguna fuente. ¿Será algún problema de configuración? Intente reinstalando el programa. - + &Quit - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - Parece que la aplicación no se cerró correctamente la última vez. ¿Desea restaurar la sesión anterior? + Parece que la aplicación no se cerró correctamente la última vez. ¿Desea restaurar la sesión anterior? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - Se detectó el complemento de Firefox "Danbooru Downloader" en el sistema. ¿Desea cargar su configuración? + Se detectó el complemento de Firefox "Danbooru Downloader" en el sistema. ¿Desea cargar su configuración? - + Don't ask me again @@ -1941,19 +1971,19 @@ Por favor, resuelva este problema antes de continuar las descarga. El límite de imágenes debe ser mayor o igual que 0. - + MM/dd/yyyy - MM/dd/yyyy + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - <b>Nombre:</b> %1<br/><b>Nota:</b> %2 %%<br/><b>Última vista:</b> %3 + <b>Nombre:</b> %1<br/><b>Nota:</b> %2 %%<br/><b>Última vista:</b> %3 - + Are you sure you want to quit? - ¿Seguro que desea salir de la aplicación? + ¿Seguro que desea salir de la aplicación? Don't keep for later @@ -2077,7 +2107,7 @@ Por favor, resuelva este problema antes de continuar las descarga. - + Choose a save folder @@ -2087,72 +2117,73 @@ Por favor, resuelva este problema antes de continuar las descarga. Md5 list fixer - Arreglar lista MD5 + Arreglar lista MD5 This tool will clear your MD5 list and fill it again with the MD5 of the files found in the folder set below. - Esta herramienta limpia la lista MD5 y la vuelve a llenar con las sumas MD5 de los archivos encontrados en la carpeta seleccionada. + Esta herramienta limpia la lista MD5 y la vuelve a llenar con las sumas MD5 de los archivos encontrados en la carpeta seleccionada. Folder - Carpeta + Carpeta Force md5 calculation - + Forzar cálculos md5 + Forzar calculo md5 Get md5 in filename - Usar MD5 en el nombre del archivo + Usar MD5 en el nombre del archivo Filename - Nombre del archivo + Nombre del archivo %v/%m - %v/%m + %v/%m Start - + Empezar Cancel - Cancelar + Cancelar Suffixes - + Sufijos - + This folder does not exist. - Esta carpeta no existe. + Esta carpeta no existe. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - Si quiere usar el MD5 en el nombre del archivo, debe incluir el identificador %md5% en él. + Si quiere usar el MD5 en el nombre del archivo, debe incluir el identificador %md5% en él. - + Finished - Finalizado + Finalizado - + %n MD5(s) loaded - + %n archivo MD5 cargado %n archivos MD5 cargados @@ -2161,24 +2192,24 @@ Por favor, resuelva este problema antes de continuar las descarga. MonitoringCenter - + New images found for tag '%1' on '%2' - + Nuevas imagenes encontradas para la etiqueta '%1' en '%2' - + %n new image(s) found for tag '%1' on '%2' - - - + + %n nueva imagen encontrada para la etiqueta '%1' en '%2' + %n nueva imagenes encontradas para la etiqueta '%1' en '%2' - + More than %n new image(s) found for tag '%1' on '%2' - - - + + Mas de %n nueva imagen encontrada para la etiqueta '%1' en '%2' + Mas de %n nuevas imagenes encontradas para la etiqueta '%1' en '%2' @@ -2192,281 +2223,275 @@ Por favor, resuelva este problema antes de continuar las descarga. Options - Opciones + Opciones General - General + General Sources - Fuentes + Fuentes Save - Guardar + Guardar Filename - Nombre del archivo + Nombre del archivo Conditional filenames - Nombres de archivo condicionales + Nombres de archivo condicionales Separate log files - + Separar los archivos de sección - Artist tags - Etiquetas de artistas + Etiquetas de artistas - Copyright tags - Etiquetas de derechos de autor + Etiquetas de derechos de autor - Character tags - Etiquetas de personajes + Etiquetas de personajes - Species tags - Etiquetas de especies + Etiquetas de especies - Meta tags - + Etiquetas priincipales - + Custom token - Identificador personalizado + Identificador personalizado - + Interface - Interfaz + Interfaz - + Search results - + Buscar resultados - + Image window - Ventana de imagen + Ventana de imagen - + Coloring - Colores + Colores - + Margins and borders - Márgenes y bordes + Márgenes y bordes - + Log - Registros + Registros - + Blacklist - Lista negra + Lista negra - + Monitoring - + Proxy - Proxy + Proxy - + Web services - + Servicios web - - + + Commands - Comandos + Comandos - - + + Database - Base de datos + Base de datos - + Language - Idioma + Idioma - + At start - Al iniciar + Al iniciar - + Do nothing - No hacer nada + No hacer nada - + Load first page - Cargar la primera página + Cargar la primera página - + Restore last session - Restaurar la sesión anterior + Restaurar la sesión anterior - + Check for updates - Buscar actualizaciones + Buscar actualizaciones - + Every time - Siempre + Siempre - + Once a day - Diariamente + Diariamente - + Once a week - Semanalmente + Semanalmente - + Once a month - Mensualmente + Mensualmente - + Never - Nunca + Nunca - + Whitelist - Lista blanca + Lista blanca - + Download - + Descargar - + Don't download automatically - No descargar automáticamente + No descargar automáticamente - + When loading image - Cuando cargue la imagen + Cuando cargue la imagen - + When loading thumbnail - Cuando cargue la miniatura + Cuando cargue la miniatura - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> - <i>Las imágenes que contengan una etiqueta de la lista blanca serán descargadas automáticamente de acuerdo a la opción elegida.</i> + <i>Las imágenes que contengan una etiqueta de la lista blanca serán descargadas automáticamente de acuerdo a la opción elegida.</i> - + Ignored tags - Etiquetas ignoradas + Etiquetas ignoradas - + <i>These tags will not be taken in account when saving image.</i> - <i>Estas etiquetas no se tomarán en cuenta al guardar la imagen.</i> + <i>Estas etiquetas no se tomarán en cuenta al guardar la imagen.</i> - + Download images containing blacklisted tags - Descargar las imágenes que contengan etiquetas de la lista negra + Descargar las imágenes que contengan etiquetas de la lista negra - + Adds - Etiquetas adicionales + Etiquetas adicionales - <i>These tags will be automatically added to every search.</i> - <i>Estas etiquetas se añadirán automáticamente en cada búsqueda.</i> + <i>Estas etiquetas se añadirán automáticamente en cada búsqueda.</i> - + Ask for confirmation before closing the window - Pedir confirmación antes de cerrar la ventana + Pedir confirmación antes de cerrar la ventana Images per page - Imágenes por página + Imágenes por página Number of columns - Número de columnas + Número de columnas Source 1 - Fuente 1 + Fuente 1 Source 2 - Fuente 2 + Fuente 2 Source 3 - Fuente 3 + Fuente 3 Source 4 - Fuente 4 + Fuente 4 Get more precise tags when searching images - Obtener etiquetas más precisas al buscar las imágenes + Obtener etiquetas más precisas al buscar las imágenes @@ -2474,7 +2499,7 @@ Por favor, resuelva este problema antes de continuar las descarga. XML - XML + XML @@ -2482,7 +2507,7 @@ Por favor, resuelva este problema antes de continuar las descarga. JSON - JSON + JSON @@ -2490,7 +2515,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Regex - Expresión regular + Expresión regular @@ -2498,85 +2523,85 @@ Por favor, resuelva este problema antes de continuar las descarga. RSS - RSS + RSS Auto tag add - Etiquetas automáticas + Etiquetas automáticas Download original images - Descargar imágenes originales + Descargar imágenes originales Download sample on error - Descargar la muestra en caso de error + Descargar la muestra en caso de error Download images automatically - Descargar imágenes automáticamente + Descargar imágenes automáticamente Keep original creation date - Mantener la fecha de creación original + Mantener la fecha de creación original Get extension from file header - Obtener extensión de la cabecera del archivo + Obtener extensión de la cabecera del archivo Folder - Carpeta + Carpeta Browse - Navegar + Navegar - + Favorites - Favoritos + Favoritos Simultaneous downloads - Descargas simultáneas + Descargas simultáneas When the download is finished - Al terminar la descarga + Al terminar la descarga Close window - Cerrar la ventana + Cerrar la ventana Open CD tray - Abrir la bandeja de CD + Abrir la bandeja de CD Play a sound - Reproducir un sonido + Reproducir un sonido Shutdown - Apagar + Apagar @@ -2587,12 +2612,12 @@ Por favor, resuelva este problema antes de continuar las descarga. Copy - Copiar + Copiar Move - Mover + Mover @@ -2603,783 +2628,775 @@ Por favor, resuelva este problema antes de continuar las descarga. Don't save - No guardar + No guardar <i>File's identity is based on the MD5 algorithm.</i> - <i>La identidad del archivo se encuentra usando el algoritmo MD5.</i> + <i>La identidad del archivo se encuentra usando el algoritmo MD5.</i> Automatic redownload - Repeticiones automáticas de descarga + Repeticiones automáticas de descarga Keep deleted files in the MD5 list - + Mantener las imagenes eliminadas en la lista MD5 If an image yields multiple files - + Si una imagen produce múltiples archivos Default - Predeterminado + Predeterminado Tags separator - Separador de etiquetas + Separador de etiquetas Replace spaces by underscores - Substituye los espacios por guiones bajos + Substituye los espacios por guiones bajos Replace JPEG by JPG - Reemplazar JPEG por JPG + Reemplazar JPEG por JPG Max length - Longitud máxima + Longitud máxima <i>If the filename length is greater than this number, it will be shortened. Leave it to 0 to use the default limit.</i> - <i>El nombre del archivo se acortará si es más grande que este número. Al dejarlo en 0 se usará el límite predeterminado.</i> + <i>El nombre del archivo se acortará si es más grande que este número. Al dejarlo en 0 se usará el límite predeterminado.</i> Add a conditional filename - Añadir nombre de archivo condicional + Añadir nombre de archivo condicional <i>Each time an image is saved, its information can be added to a separate text file for later processing or for organization purposes.</i> - <i>Cada vez que se guarde una imagen, se creará un archivo de texto adicional con el mismo nombre en el mismo lugar, conteniendo las etiquetas de la imagen.</i> + <i>Cada vez que se guarde una imagen, se creará un archivo de texto adicional con el mismo nombre en el mismo lugar, conteniendo las etiquetas de la imagen.</i> Add a separate log file - + Añadir una carpeta de sesión - - - - - If empty - Si está vacío + Si está vacío - - - - - Separator - Separador + Separador - - - - - If more than n tags - Si el número de etiquetas es mayor que + Si el número de etiquetas es mayor que - - - - - Keep n tags, then add - Mantener un número de etiquetas y luego añadir + Mantener un número de etiquetas y luego añadir - - - - - Replace all tags by - Reemplazar todas las etiquetas por + Reemplazar todas las etiquetas por - - - - - Keep n tags - Mantener un número de etiquetas + Mantener un número de etiquetas - - - - - Keep all tags - Mantener todas las etiquetas + Mantener todas las etiquetas - - - - - One file per tag - Un archivo por etiqueta + Un archivo por etiqueta + + + Original + Original + + + Sort + Ordenar - Use shortest if possible - Usar la etiqueta más corta + Usar la etiqueta más corta - + Add a custom token - Añadir un identificador personalizado + Añadir un identificador personalizado - + Theme - + Temas - + Upscaling - Ampliación + Ampliación - + % - % + % - + Favorites display - Visualización de favoritos + Visualización de favoritos - + Image, name and details - Imagen, nombre y detalles + Imagen, nombre y detalles - + Image and name - Imagen y nombre + Imagen y nombre - + Image and details - Imagen y detalles + Imagen y detalles - + Name and details - Nombre y detalles + Nombre y detalles - + Image only - Solo la imagen + Solo la imagen - + Name only - Solo el nombre + Solo el nombre - + Details only - Solo los detalles + Solo los detalles - + Hide favorites - Esconder los favoritos + Esconder los favoritos - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> - <i>La lista de favoritos se esconderá al alcanzar este número de imágenes.</i> + <i>La lista de favoritos se esconderá al alcanzar este número de imágenes.</i> - + Source's type display - Tipo de visualización de la fuente + Tipo de visualización de la fuente - + Text - Texto + Texto - - - + + + Image - Imagen + Imagen - + Image and text - Imagen y texto + Imagen y texto - + Don't show - No mostrar + No mostrar - + Displayed letters - Letras mostradas + Letras mostradas - + Display n letters - Mostrar un número de letras + Mostrar un número de letras - + Before first dot - Antes del primer punto + Antes del primer punto - + Before last dot - Antes del último punto + Antes del último punto - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> - <i>El número de letras que se mostrará junto a las casillas de las fuentes en la ventana principal.</i> + <i>El número de letras que se mostrará junto a las casillas de las fuentes en la ventana principal.</i> - + Preload all tabs when restoring a previous session - + Precargar todas las etiquetas cuando restaures una sesión previa - + Use a scroll area - + Use a fixed-image-width layout - + Infinite scroll - + Disabled - + Desactivado - + Button - + Botón - + Scroll - + Scroll - + Remember page number when infinite scrolling - + Resize previews instead of cropping them - Redimensionar las vistas previas en vez de recortarlas + Redimensionar las vistas previas en vez de recortarlas - + Enable autocompletion - Habilitar el autocompletado + Habilitar el autocompletado - + Show warning if an incompatible modifier is found - Mostrar advertencia si se encuentra un modificador incompatible + Mostrar advertencia si se encuentra un modificador incompatible - + Show other warnings - Mostrar otras advertencias + Mostrar otras advertencias - + Download not loaded pages - Descargar páginas no cargadas + Descargar páginas no cargadas - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> - <i>Si esta opción está activa, al presionar el botón "Obtener esta página" se tomarán en cuenta las modificaciones hechas al número de imágenes por página, el número de la página, etc. incluso si no pudo cargarse.</i> + <i>Si esta opción está activa, al presionar el botón "Obtener esta página" se tomarán en cuenta las modificaciones hechas al número de imágenes por página, el número de la página, etc. incluso si no pudo cargarse.</i> - + Invert Click and Ctrl+Click actions - + Invertir Click y acciones Ctrl + Click - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + <i>Con esta opcion activada, clicar sobre una imagen lo marcará para descargar, mientras Ctrl+Click abrirá una ventana con los detalles</i> - + Tag list position - Posición de la lista de etiquetas + Posición de la lista de etiquetas - - - - + + + + Top - Arriba + Arriba - - - - + + + + Left - Izquierda + Izquierda - + Auto - Automático + Automático - + Preloading - Precarga + Precarga - + Slideshow - Presentación + Presentación - + s - s + s - + Middle click to close window - Clic con el botón central para cerrar la ventana + Clic con el botón central para cerrar la ventana - + Enable scroll wheel navigation - Habilitar navegación con la rueda de desplazamiento + Habilitar navegación con la rueda de desplazamiento - + Show tag count - + Enseñar el numero de etiquetas - + Tag order - + Ordenar las etiquetas - - + + Type - Tipo + Tipo - + Name - Nombre + Nombre - + Count - + Contar - + Image position - Posición de la imagen + Posición de la imagen - - - - - - + + + + + + Center - Centro + Centro - - - + + + Bottom - Abajo + Abajo - - - + + + Right - Derecha + Derecha - + Animation position - Posición de la animación + Posición de la animación - + Video position - Posición del video + Posición del video - + Background color - + Color de fondo - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color - Color + Color - + Use a single image window + Usar una ventana de una sola imagen + + + + Tags + Etiquetas + + + + <i>These tags and post-filters will be automatically added to every search.</i> + + + + + Post-filters - + + Send anonymous usage data + + + + + Use image samples + + + + Artists - Artistas - - - - - - - - - - - - - - + Artistas + + + + + + + + + + + + + + Font - Tipografía + Tipografía - + Circle - Círculo + Círculo - + Series - Series + Series - + Characters - Personajes + Personajes - + Models - Modelos + Modelos - + Generals - Generales + Generales - + Blacklisted - En la lista negra + En la lista negra - + Ignored - Ignorados + Ignorados - + Species - Especies + Especies - + Kept for later - + Metas - + Hosts - Servidores + Servidores - - + + Horizontal margins - Márgenes horizontales + Márgenes horizontales - - + + Borders - Bordes + Bordes - + Images - Imágenes + Imágenes - + Vertical margins - Márgenes verticales + Márgenes verticales - + Show log - Mostrar registros + Mostrar registros - + Blacklisted tags - + Etiquetas en Lista Negra - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + <i>Una linea por lista negra. Tu puedes poner multiples etiquetas en una sola linea para hacer condiciones "AND".</i> - + Ignore images containing a blacklisted tag - Ignorar las imágenes que contengan una etiqueta de la lista negra + Ignorar las imágenes que contengan una etiqueta de la lista negra - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> - <i>Cuando esté activado, las imágenes que contengan una etiqueta de la lista negra no se mostrarán. De otra forma, se le preguntará antes de mostrar una de estas imágenes.</i> + <i>Cuando esté activado, las imágenes que contengan una etiqueta de la lista negra no se mostrarán. De otra forma, se le preguntará antes de mostrar una de estas imágenes.</i> - + Delay on startup - + Retrasar al ejecutar - + s - + s - + Tray icon - + Trazar icono - + Minimize to tray - + Minimzar para trazari - + Close to tray - + Cerrar para trazar - + Enable system tray icon - + Use proxy - Usar proxy + Usar proxy - + HTTP - HTTP + HTTP - + SOCKS v5 - SOCKS v5 + SOCKS v5 - - + + Host - Servidor + Servidor - + Port - Puerto + Puerto - - + + User - Usuario + Usuario - - + + Password - Contraseña + Contraseña - + Use system-wide proxy settings - Usar la configuración del sistema + Usar la configuración del sistema - + Add a web service - + Añadir un servicio web - - + + Tag (after) - Etiqueta (después) + Etiqueta (después) - - + + Tag (before) - Etiqueta (antes) + Etiqueta (antes) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) - Etiquetas adicionales: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: la etiqueta<br/><i>%type%</i>: tipo de etiqueta, "general", "artist", "copyright", "character", "model" o "photo_set"<br/><i>%number%</i>: el número del tipo de etiqueta (entre 0 y 6) + Etiquetas adicionales: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: la etiqueta<br/><i>%type%</i>: tipo de etiqueta, "general", "artist", "copyright", "character", "model" o "photo_set"<br/><i>%number%</i>: el número del tipo de etiqueta (entre 0 y 6) - + Start - + Empezar - + End - Final + Final - + Credentials - Identificación + Identificación - + Driver - Controlador + Controlador - + Choose a save folder - + Elegir una carpeta de guardado - + Choose a save folder for favorites - Elija una carpeta para guardar los favoritos + Elija una carpeta para guardar los favoritos - - + + Edit - + Editar - - + + Remove - Eliminar + Eliminar - + Choose a color - Elija un color + Elija un color - + Choose a font - Elija una tipografía + Elija una tipografía - + An error occured creating the save folder. - Ha ocurrido un error al crear la carpeta para guardar. + Ha ocurrido un error al crear la carpeta para guardar. - + An error occured creating the favorites save folder. - Ha ocurrido un error al crear la carpeta para guardar los favoritos. + Ha ocurrido un error al crear la carpeta para guardar los favoritos. Page - + No valid source of the site returned result. No se obtuvo resultados de ninguna de las fuentes del sitio web. @@ -3398,54 +3415,53 @@ Por favor, resuelva este problema antes de continuar las descarga. PoolTab - New pool tab - Nueva pestaña de colecciones + Nueva pestaña de colecciones - + Pl&us - &Más + &Más - + O&k - Acepta&r + Acepta&r - + Maybe you meant: - Quizás quiso decir: + Quizás quiso decir: - + Images per page - Imágenes por página + Imágenes por página - + Number of columns - Número de columnas + Número de columnas - + Post-filtering - Después del filtro + Después del filtro - + Get &selected - Obtener &seleccionado + Obtener &seleccionado - + Get this &page - + Obtener esta &pagina - + Get &all - Obtener &todo + Obtener &todo @@ -3521,47 +3537,46 @@ Por favor, resuelva este problema antes de continuar las descarga. <b>Aviso:</b> %1 - MM-dd-yyyy HH.mm - MM-dd-yyyy HH.mm + MM-dd-yyyy HH.mm - + Filename must not be empty! ¡El nombre de archivo no debe estar vacío! - + Can't validate Javascript expressions. No se pueden validar las expresiones Javascript. - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. ¡El nombre de archivo no termina con una extensión, identificada con %ext%! Es posible que no pueda abrir los archivos guardados. - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. ¡El nombre de archivo no es único para cada imagen y se pueden sobreescribir al guardarse! Debería usar %md5%, lo cual es único para cada imagen para evitar este inconveniente. - + The %%1% token does not exist and will not be replaced. El identificador %%1% no existe y no será reemplazado. - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | ¡El formato contiene caracteres prohibidos en Windows! Caracteres prohibidos: * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. Está utilizando el identificador %id%. Este solo es único para el sitio web seleccionado. La misma ID puede identificar imágenes diferentes dependiendo del sitio web. - + Valid filename! ¡Nombre de archivo válido! @@ -3594,12 +3609,12 @@ Por favor, resuelva este problema antes de continuar las descarga. GiB - + image has a "%1" token - + image does not have a "%1" token @@ -3608,53 +3623,53 @@ Por favor, resuelva este problema antes de continuar las descarga. Tipo desconocido "%1" (tipos disponibles: "%2") - - - + + + image's %1 does not match %1 de la imagen no coincide - - - + + + image's %1 match %1 de la imagen coincide - - + + image is not "%1" la imagen no es "%1" - - + + image is "%1" la imagen no es "%1" - + An image needs a date to be filtered by age - + image's source does not starts with "%1" la fuente de la imagen no comienza con "%1" - + image's source starts with "%1" la fuente de la imagen no comienza con "%1" - + image does not contains "%1" la imagen no contiene "%1" - + image contains "%1" la imagen contiene "%1" @@ -3663,7 +3678,7 @@ Por favor, resuelva este problema antes de continuar las descarga. RenameExisting1 - + Rename existing images Renombrar imágenes existentes @@ -3715,20 +3730,20 @@ Por favor, resuelva este problema antes de continuar las descarga. Suffixes - + Sufijos - + This folder does not exist. Esta carpeta no existe. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. Si quiere usar el MD5 en el nombre del archivo, debe incluir el identificador %md5% en él. - + You are about to download information from %n image(s). Are you sure you want to continue? Está a punto de descargar información de %n imagen. ¿Desea continuar? @@ -3736,9 +3751,9 @@ Por favor, resuelva este problema antes de continuar las descarga. - + No image found when renaming image '%1' - + Ninguna imagen encontrada cuando se renombraba la imagen '%1' @@ -3782,107 +3797,112 @@ Por favor, resuelva este problema antes de continuar las descarga. SearchTab - + + all images filtered + + + + server offline - servidor fuera de línea + servidor fuera de línea - + too many tags - demasiadas etiquetas + demasiadas etiquetas - + page too far - página demasiado lejos + página demasiado lejos - + HTTPS redirection detected - + Redirección segura detectada (HTTPS) - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Una redireccion de HTTPS a HTTPS ha sido detectada por la página %1. ¿Quieres activar SSL en ella? La configuracion recomendada es 'Sí'. - + Always - + Siempre - + Never for that website - + Nunca para esa pagina erb - + Never - Nunca + Nunca - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - Algunas etiquetas de la imagen se encuentran en la lista blanca: %1. Sin embargo, algunas etiquetas se encuentran en la lista negra: %2. ¿Desea descargarla de todas formas? + Algunas etiquetas de la imagen se encuentran en la lista blanca: %1. Sin embargo, algunas etiquetas se encuentran en la lista negra: %2. ¿Desea descargarla de todas formas? - - + + Page %1 of %2 (%3 of %4) - Página %1 de %2 (%3 de %4) + Página %1 de %2 (%3 de %4) - - - + + + max %1 - + máximo %1 - + No result - Sin resultados + Sin resultados - + Possible reasons: %1 - Razones posibles: %1 + Razones posibles: %1 - + Delete - Eliminar + Eliminar - + Save - Guardar + Guardar - + Save as... - Guardar como... + Guardar como... - + Save selected - Guardar seleccionado + Guardar seleccionado - + Save image - Guardar imagen + Guardar imagen - + Blacklist - Lista negra + Lista negra - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? - + se detectó %n etiqueta de esta imagen en la lista negra: %1. ¿Desea mostrarla de todas formas? se detectaron %n etiquetas de esta imagen en la lista negra: %1. ¿Desea mostrarla de todas formas? @@ -4051,7 +4071,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Elija una fecha - + Search an image Buscar una imagen @@ -4100,7 +4120,7 @@ Por favor, resuelva este problema antes de continuar las descarga. - + Unable to guess site's type. Are you sure about the url? No se pudo encontrar el tipo del sitio web. ¿La URL es correcta? @@ -4173,8 +4193,8 @@ Por favor, resuelva este problema antes de continuar las descarga. - - + + Name Nombre @@ -4204,17 +4224,16 @@ Por favor, resuelva este problema antes de continuar las descarga. Descargas simultaneas máximas - Images per page - Imágenes por página + Imágenes por página - + Interval (thumbnail) Intervalo (miniatura) - + Interval (image) Intervalo (imagen) @@ -4224,116 +4243,111 @@ Por favor, resuelva este problema antes de continuar las descarga. Intervalo (página) - + Interval (details) Intervalo (detalles) - + Interval (error) Intervalo (error) - - + + + + - - s s - + Sources Fuentes - + Source 1 Fuente 1 - - - - + + + + XML XML - - - - + + + + JSON JSON - - - - + + + + Regex Expresión regular - - - - + + + + RSS RSS - + Source 2 Fuente 2 - + Source 3 Fuente 3 - + Source 4 Fuente 4 - + Use default sources Usar fuentes predeterminadas - Credentials - Identificación + Identificación - - + Username Usuario - - + Password Contraseña - Hash password - Cifrar contraseña + Cifrar contraseña - - + Test Prueba - + Login Inicio de sesión @@ -4346,183 +4360,162 @@ Por favor, resuelva este problema antes de continuar las descarga. Método - + GET GET - + POST POST - - URL - URL + URL - - Cookie - Cookie + Cookie - Page limit - Límite de páginas + Límite de páginas - + Type Tipo - + Through URL A través de URL - + OAuth 1 - + OAuth 2 - Username field - + Campo de usuario - Password field - + Campo de contraseña - - Request token url - - - - Authorize url - + Autorizar URL - - Access token url - - - - Request url - - - - - Token url - - - - - Refresh token url - - - - - Scope - + Pedir url - + Cookies Cookies - - + + Value Valor - - + + Add Añadir - + Headers Cabeceras - + Delete Eliminar - + Cancel Cancelar - + Confirm Aceptar - + + API key + + + + + Consumer key + + + + + Consumer secret + + + Hash a password - Cifrar una contraseña + Cifrar una contraseña - Please enter your password below.<br/>It will then be hashed using the format "%1". - Por favor introduzca su contraseña abajo. </br>Se cifrará usando el formato "%1". + Por favor introduzca su contraseña abajo. </br>Se cifrará usando el formato "%1". - + Delete a site Eliminar un sitio web - + Are you sure you want to delete the site %1? ¿Seguro que quiere eliminar el sitio web %1? - + Connection... - + Conectando... - + Success! ¡Éxito! - + Failure Error - + Unable to test No es posible hacer la prueba - + Error - Error + Error - + You should at least select one source - + Deberías al menos seleccionar una fuente @@ -4530,61 +4523,61 @@ Por favor, resuelva este problema antes de continuar las descarga. Sources - Fuentes + Fuentes Check all - Seleccionar todo + Seleccionar todo ... - ... + ... Add - Añadir + Añadir Cancel - Cancelar + Cancelar Ok - Aceptar + Aceptar - + Options - Opciones + Opciones - + An update for this source is available. - Hay una actualización disponible para esta fuente. + Hay una actualización disponible para esta fuente. - + - No preset selected - - + Create a new preset - - + + Name - Nombre + Nombre - + Edit preset @@ -4594,47 +4587,47 @@ Por favor, resuelva este problema antes de continuar las descarga. First launch - Primera ejecución + Primera ejecución Before starting, the program needs some informations to work properly. You can skip this step, and these informations will be asked later. - Antes de comenzar, el programa necesita cierta información para funcionar correctamente. Puede saltarse este paso, y se le pedirá más tarde. + Antes de comenzar, el programa necesita cierta información para funcionar correctamente. Puede saltarse este paso, y se le pedirá más tarde. Language - Idioma + Idioma Folder - Carpeta + Carpeta Browse - Navegar + Navegar Format - Formato + Formato ... - ... + ... Source - Fuente + Fuente <i>If you use Grabber for the first time, it is advised to first read the <a href="{website}/docs/">getting started</a> wiki page.</i> - + <i>Si estás usando Grabber por primera vez,te recomendamos primero leer la <a href="{website}/docs/">Primeros pasos</a> pagina de la wiki.</i> <i>If you use Grabber for the first time, it is advised to first read the <a href="{github}/wiki/GettingStarted">getting started</a> wiki page.</i> @@ -4643,33 +4636,33 @@ Por favor, resuelva este problema antes de continuar las descarga. Options - Opciones + Opciones - + Choose a save folder - + Elije una carpeta de guardado - + An error occurred creating the save folder. - + Ha ocurrido un error al crear la carpeta de guardado. TagContextMenu - + Remove from favorites Eliminar de favoritos - + Choose as image Elegir como imagen - + Add to favorites Añadir a favoritos @@ -4684,47 +4677,47 @@ Por favor, resuelva este problema antes de continuar las descarga. Guardar para más tarde - + Don't blacklist - + No meter a la lista negra - + Blacklist Lista negra - + Don't ignore No ignorar - + Ignore Ignorar - + Copy tag Copiar etiqueta - + Copy all tags Copiar todas las etiquetas - + Open in a new tab Abrir en una pestaña nueva - + Open in new a window Abrir en una ventana nueva - + Open in browser Abrir en el navegador web @@ -4742,7 +4735,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Tag loader - + Cargador de etiquetas @@ -4762,7 +4755,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Generate the local tag database for a given source. Afterwards, even if the source's API does not provide tag type information, Grabber can directly check it in its local tag database. - + Genera la etiqueta de la base de datos con una fuente preestablecida. Despues, incluso si la API de la fuente no proporciona un tipo de informacion de la etiqueta, Grabber podrá comprobar directamente en su base de datos de etiquetas. @@ -4770,95 +4763,94 @@ Por favor, resuelva este problema antes de continuar las descarga. Fuente - + Finished Finalizado - + %n tag(s) loaded - - - + + %n etiqueta cargada + %n etiquetas cargadas TagTab - New tab - Nueva pestaña + Nueva pestaña - + Pl&us - &Más + &Más - + O&k - Acepta&r + Acepta&r - + Maybe you meant: - Quizás quiso decir: + Quizás quiso decir: - + Post-filtering - Después del filtro + Después del filtro - + How many sources should appear per line. - + Cuentas fuentes debería aparecer por línea. - + Number of columns - Número de columnas + Número de columnas - + Images per page - Imágenes por página + Imágenes por página - + Load more results - + Cargar más resultados - + S&ources - &Fuentes + &Fuentes - + &Merge results - &Combinar resultados + &Combinar resultados - + Get &selected - Obtener &seleccionado + Obtener &seleccionado - + Get this &page - + Obtener esta &página - + Get &all - Obtener &todo + Obtener &todo - + Search - Buscar + Buscar @@ -4870,47 +4862,115 @@ Por favor, resuelva este problema antes de continuar las descarga. - + Remove Eliminar - + Add Añadir - + Kept for later Guardar para más tarde - + Ratings Clasificación - + Sortings Ordenar por - + Copy Copiar - + Cut Cortar - + Paste Pegar + + TokenSettingsWidget + + + Form + Form + + + + If empty + Si está vacío + + + + Separator + Separador + + + + Sort + Ordenar + + + + Original + Original + + + + Name + Nombre + + + + If more than n tags + Si el número de etiquetas es mayor que + + + + Keep all tags + Mantener todas las etiquetas + + + + Keep n tags + Mantener un número de etiquetas + + + + Keep n tags, then add + Mantener un número de etiquetas y luego añadir + + + + Replace all tags by + Reemplazar todas las etiquetas por + + + + One file per tag + Un archivo por etiqueta + + + + Use shortest if possible + Usar la etiqueta más corta + + UpdateDialog @@ -4929,7 +4989,7 @@ Por favor, resuelva este problema antes de continuar las descarga. Ver los cambios - + Version <b>%1</b> Versión <b>%1</b> @@ -4960,14 +5020,14 @@ Por favor, resuelva este problema antes de continuar las descarga. ZoomWindow - - + + Image Imagen - + Save Guardar @@ -4978,7 +5038,7 @@ Por favor, resuelva este problema antes de continuar las descarga. - + Save and close Guardar y cerrar @@ -4994,13 +5054,13 @@ Por favor, resuelva este problema antes de continuar las descarga. - + Save (fav) Guardar (favorito) - + Save and close (fav) Guardar y cerrar (favorito) @@ -5010,134 +5070,135 @@ Por favor, resuelva este problema antes de continuar las descarga. Carpeta de destino (favorito) - + Reload - + Recargar - + Copy file Copiar archivo - + Copy data Copiar datos - + Folder does not exist La carpeta no existe - + The save folder does not exist yet. Create it? La carpeta para guardar no existe. ¿Desea crearla? - + Error creating folder. %1 Ocurrió un error al crear la carpeta. %1 - - + + Saving... (fav) Guardando... (favorito) - - + + Saving... Guardando... - + Saved! (fav) ¡Guardado! (favorito) - + Saved! ¡Guardado! - + Copied! (fav) ¡Copiado! (favorito) - + Copied! ¡Copiado! - + Moved! (fav) ¡Movido! (favorito) - + Moved! ¡Movido! - + Link created! (fav) - + ¡Enlace creado! (favorito) - + Link created! - + ¡Enlace creado! - + MD5 already exists (fav) - + MD5 ya existe (favorito) - + MD5 already exists - + MD5 ya existe - + Already exists (fav) - + Ya existe (favorito) - + Already exists - + Ya existe - + Delete (fav) - + Eliminar (favorito) - + Delete Eliminar - + Close (fav) Cerrar (favorito) - + Close Cerrar - + File is too big to be displayed. %1 - + La carpeta es demasiado grande para ser presentada. +%1 An unexpected error occured loading the image (%1 - %2). @@ -5146,28 +5207,28 @@ Por favor, resuelva este problema antes de continuar las descarga. %3 - - + + Error Error - + You did not specified a save folder! Do you want to open the options window? ¡No se especificó una carpeta para guardar! ¿Desea abrir la ventana de opciones? - + You did not specified a save format! Do you want to open the options window? ¡No se especificó un formato de guardado! ¿Desea abrir la ventana de opciones? - + Error saving image. Error al guardar la imagen. - + Save image Guardar imagen diff --git a/languages/YourLanguage.ts b/languages/YourLanguage.ts index a9a6051ee..4d5ec932a 100644 --- a/languages/YourLanguage.ts +++ b/languages/YourLanguage.ts @@ -24,12 +24,12 @@ - + A new version is available: %1 - + Grabber is up to date @@ -85,47 +85,63 @@ - + Add - + Site - + Id - + Md5 - + Filename - + + <i>One ID per line.</i> + + + + + + + + + + + + <i>One MD5 per line.</i> + + + + Folder - + Browse - + Choose a save folder - + No image found. @@ -219,7 +235,7 @@ - + Pause @@ -230,45 +246,45 @@ - + Cancel - + Paused - + Resume - - + + h 'h' m 'm' s 's' - - + + m 'm' s 's' - - + + s 's' - + <b>Average speed:</b> %1 %2<br/><br/><b>Elapsed time:</b> %3<br/><b>Remaining time:</b> %4 - + Close @@ -277,7 +293,7 @@ BlacklistFix1 - + Blacklist fixer @@ -327,17 +343,17 @@ - + This directory does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + You are about to download information from %n image(s). Are you sure you want to continue? @@ -475,7 +491,7 @@ - + Tags @@ -501,13 +517,13 @@ - + Filename - + Folder @@ -523,274 +539,286 @@ + Galleries count as one + + + + Progress - - + + Add - + Single images - + Id - + Md5 - + Rating - + Url - + Date - + Search - + Site - + Delete all - + Delete selected - + Download - + Download selected - + Move down - + Load - + Save - + Move up - + Confirmation - + Are you sure you want to clear your download list? - + This source is not valid. - + The image per page value must be greater or equal to 1. - + The image limit must be greater or equal to 0. - + Groups (%1/%2) - - - + + + Save link list - - + + Imageboard-Grabber links (*.igl) - + Link list saved successfully! - + Error opening file. - - - + + + Load link list - + Link list loaded successfully! - + Loading %n download(s) - + You did not specify a save folder! - + You did not specify a filename! - + You are going to download up to %1 images, which can take a long time and space on your computer. Are you sure you want to proceed? - + Don't ask me again - + Logging in, please wait... - + Downloading pages, please wait... - + Preparing images, please wait... - + Downloading images... - + Not enough space on the destination drive "%1". Please free some space before resuming the download. - + An error occured saving the image. %1 Please solve the issue before resuming the download. - + Error - - + + Getting images - + Errors occured during the images download. Do you want to restart the download of those images? (%1/%2) - + %n file(s) downloaded successfully. - + %n file(s) ignored. - + %n file(s) already existing. - + %n file(s) not found on the server. - + %n file(s) skipped. - + + %n file(s) skipped from a previous download. + + + + + + %n error(s). @@ -801,7 +829,7 @@ Please solve the issue before resuming the download. EmptyDirsFix1 - + Empty folders fixer @@ -821,7 +849,7 @@ Please solve the issue before resuming the download. - + No empty folder found. @@ -830,8 +858,8 @@ Please solve the issue before resuming the download. EmptyDirsFix2 - - + + Empty folders fixer @@ -851,12 +879,12 @@ Please solve the issue before resuming the download. - + No folder selected. - + You are about to delete %n folder. Are you sure you want to continue? @@ -961,7 +989,7 @@ Please solve the issue before resuming the download. - + Choose an image @@ -970,7 +998,8 @@ Please solve the issue before resuming the download. FavoritesTab - + + Favorites @@ -1025,74 +1054,74 @@ Please solve the issue before resuming the download. - + Back - + Mark as &viewed - + Get &selected - + Get this &page - + Get &all - + S&ources - + Merge results - + Mark all as vie&wed - + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %<br/><b>Last view:</b> %3 - - + + No result since the %1 - - + + MM/dd/yyyy 'at' hh:mm - + Mark as viewed - + Are you sure you want to mark all your favorites as viewed? @@ -1115,12 +1144,12 @@ Please solve the issue before resuming the download. - + Warning - + You script contains error, are you sure you want to save it? @@ -1128,42 +1157,37 @@ Please solve the issue before resuming the download. GalleryTab - - New pool tab - - - - + O&k - + Post-filtering - + Number of columns - + Images per page - + Get &selected - + Get this &page - + Get &all @@ -1171,170 +1195,170 @@ Please solve the issue before resuming the download. Image - + <b>Tags:</b> %1<br/><br/> - - + + <b>ID:</b> %1<br/> - + <b>Name:</b> %1<br/> - + <b>Rating:</b> %1<br/> - + <b>Score:</b> %1<br/> - + <b>User:</b> %1<br/><br/> - + <b>Size:</b> %1 x %2<br/> - + <b>Filesize:</b> %1 %2<br/> - + <b>Date:</b> %1 - + 'the 'MM/dd/yyyy' at 'hh:mm - + <i>Unknown</i> - + yes - + no - + Tags - + ID - + MD5 - + Rating - + Score - + Author - + Date - + 'the' MM/dd/yyyy 'at' hh:mm - + Size - + Filesize - + Page - + URL - + Source(s) - + Sample - + Thumbnail - + Parent - + yes (#%1) - + Comments - + Children - + Notes @@ -1352,7 +1376,7 @@ Please solve the issue before resuming the download. - + Search MD5 @@ -1568,6 +1592,7 @@ Please solve the issue before resuming the download. + New tab @@ -1632,52 +1657,52 @@ Please solve the issue before resuming the download. - + No source found - + No source found. Do you have a configuration problem? Try to reinstall the program. - + &Quit - + It seems that the application was not properly closed for its last use. Do you want to restore your last session? - + The Mozilla Firefox addon "Danbooru Downloader" has been detected on your system. Do you want to load its preferences? - + Don't ask me again - + MM/dd/yyyy - + <b>Name:</b> %1<br/><b>Note:</b> %2 %%<br/><b>Last view:</b> %3 - + Are you sure you want to quit? - + Choose a save folder @@ -1735,22 +1760,22 @@ Please solve the issue before resuming the download. - + This folder does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + Finished - + %n MD5(s) loaded @@ -1760,19 +1785,19 @@ Please solve the issue before resuming the download. MonitoringCenter - + New images found for tag '%1' on '%2' - + %n new image(s) found for tag '%1' on '%2' - + More than %n new image(s) found for tag '%1' on '%2' @@ -1824,210 +1849,185 @@ Please solve the issue before resuming the download. - Artist tags - - - - - Copyright tags - - - - - Character tags - - - - - Species tags - - - - - Meta tags - - - - Custom token - + Interface - + Search results - + Image window - + Coloring - + Margins and borders - + Log - + Blacklist - + Monitoring - + Proxy - + Web services - - + + Commands - - + + Database - + Language - + At start - + Do nothing - + Load first page - + Restore last session - + Check for updates - + Every time - + Once a day - + Once a week - + Once a month - + Never - + Whitelist - + Download - + Don't download automatically - + When loading image - + When loading thumbnail - + <i>Images containing a whitelisted tag will be downloaded automatically according to the option above.</i> - + Ignored tags - + <i>These tags will not be taken in account when saving image.</i> - + Download images containing blacklisted tags - + Adds - - <i>These tags will be automatically added to every search.</i> + + Ask for confirmation before closing the window - - Ask for confirmation before closing the window + + Send anonymous usage data @@ -2141,7 +2141,7 @@ Please solve the issue before resuming the download. - + Favorites @@ -2269,706 +2269,649 @@ Please solve the issue before resuming the download. - - - - - If empty - - - - - - - - - Separator - - - - - - - - - If more than n tags - - - - - - - - - Keep n tags, then add - - - - - - - - - Replace all tags by - - - - - - - - - Keep n tags - - - - - - - - - Keep all tags - - - - - - - - - One file per tag - - - - - Use shortest if possible - - - - Add a custom token - + Theme - + Upscaling - + % - + Favorites display - + Image, name and details - + Image and name - + Image and details - + Name and details - + Image only - + Name only - + Details only - + Hide favorites - + <i>The favorites list will be hidden as soon as this image number has been reached.</i> - + Source's type display - + Text - - - + + + Image - + Image and text - + Don't show - + Displayed letters - + Display n letters - + Before first dot - + Before last dot - + <i>Number of displayed letters near the sources' checkboxes in the "+" part of the main window.</i> - + Preload all tabs when restoring a previous session - + Use a scroll area - + Use a fixed-image-width layout - + Infinite scroll - + Disabled - + Button - + Scroll - + Remember page number when infinite scrolling - + Resize previews instead of cropping them - + Enable autocompletion - + Show warning if an incompatible modifier is found - + Show other warnings - + Download not loaded pages - + <i>If you activate this option, pressing the "Get this page" button will take into account modifications made to the number of images per page, the page number, etc. even if they weren't loaded.</i> - + Invert Click and Ctrl+Click actions - + <i>With this option enabled, clicking an image will mark it for download, while Ctrl+Click will open the details window.</i> - + Tag list position - - - - + + + + Top - - - - + + + + Left - + Auto - + Preloading - + Slideshow - + s - + Middle click to close window - + Enable scroll wheel navigation - + Show tag count - + Tag order - - + + Type - + Name - + Count - + Image position - - - - - - + + + + + + Center - - - + + + Bottom - - - + + + Right - + Animation position - + Video position - + Background color - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + Color - + Use a single image window - + + Tags + + + + + <i>These tags and post-filters will be automatically added to every search.</i> + + + + + Post-filters + + + + + Use image samples + + + + Artists - - - - - - - - - - - - + + + + + + + + + + + + Font - + Circle - + Series - + Characters - + Models - + Generals - + Blacklisted - + Ignored - + Species - + Kept for later - + Metas - + Hosts - - + + Horizontal margins - - + + Borders - + Images - + Vertical margins - + Show log - + Blacklisted tags - + <i>One line per blacklist. You can put multiple tags on a single line to make "AND" conditions.</i> - + Ignore images containing a blacklisted tag - + <i>Images containing a blacklisted tag will not be displayed in the results if this box is checked. Else, a confirmation will be asked before showing one of these images.</i> - + Delay on startup - + s - + Tray icon - + Minimize to tray - + Close to tray - + Enable system tray icon - + Use proxy - + HTTP - + SOCKS v5 - - + + Host - + Port - - + + User - - + + Password - + Use system-wide proxy settings - + Add a web service - - + + Tag (after) - - + + Tag (before) - - + + Additional tags: <i>%tag%</i>, <i>%type%</i>, <i>%number%</i>.<br/><i>%tag%</i>: the tag<br/><i>%type%</i>: tag type, "general", "artist", "copyright", "character", "model" or "photo_set"<br/><i>%number%</i>: the tag type number (between 0 and 6) - + Start - + End - + Credentials - + Driver - + Choose a save folder - + Choose a save folder for favorites - - + + Edit - - + + Remove - + Choose a color - + Choose a font - + An error occured creating the save folder. - + An error occured creating the favorites save folder. @@ -2976,7 +2919,7 @@ Please solve the issue before resuming the download. Page - + No valid source of the site returned result. @@ -2984,52 +2927,47 @@ Please solve the issue before resuming the download. PoolTab - - New pool tab - - - - + Pl&us - + O&k - + Maybe you meant: - + Images per page - + Number of columns - + Post-filtering - + Get &selected - + Get this &page - + Get &all @@ -3090,47 +3028,42 @@ Please solve the issue before resuming the download. QObject - - MM-dd-yyyy HH.mm - - - - + Filename must not be empty! - + Can't validate Javascript expressions. - + Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files. - + Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience. - + The %%1% token does not exist and will not be replaced. - + Your format contains characters forbidden on Windows! Forbidden characters: * ? " : < > | - + You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site. - + Valid filename! @@ -3140,63 +3073,63 @@ Please solve the issue before resuming the download. - + image has a "%1" token - + image does not have a "%1" token - - - + + + image's %1 does not match - - - + + + image's %1 match - - + + image is not "%1" - - + + image is "%1" - + An image needs a date to be filtered by age - + image's source does not starts with "%1" - + image's source starts with "%1" - + image does not contains "%1" - + image contains "%1" @@ -3205,7 +3138,7 @@ Please solve the issue before resuming the download. RenameExisting1 - + Rename existing images @@ -3260,24 +3193,24 @@ Please solve the issue before resuming the download. - + This folder does not exist. - + If you want to get the MD5 from the filename, you have to include the %md5% token in it. - + You are about to download information from %n image(s). Are you sure you want to continue? - + No image found when renaming image '%1' @@ -3323,105 +3256,110 @@ Please solve the issue before resuming the download. SearchTab - + + all images filtered + + + + server offline - + too many tags - + page too far - + HTTPS redirection detected - + An HTTP to HTTPS redirection has been detected for the website %1. Do you want to enable SSL on it? The recommended setting is 'yes'. - + Always - + Never for that website - + Never - + Some tags from the image are in the whitelist: %1. However, some tags are in the blacklist: %2. Do you want to download it anyway? - - + + Page %1 of %2 (%3 of %4) - - - + + + max %1 - + No result - + Possible reasons: %1 - + Delete - + Save - + Save as... - + Save selected - + Save image - + Blacklist - + %n tag figuring in the blacklist detected in this image: %1. Do you want to display it anyway? @@ -3591,7 +3529,7 @@ Please solve the issue before resuming the download. - + Search an image @@ -3629,7 +3567,7 @@ Please solve the issue before resuming the download. - + Unable to guess site's type. Are you sure about the url? @@ -3702,8 +3640,8 @@ Please solve the issue before resuming the download. - - + + Name @@ -3734,16 +3672,11 @@ Please solve the issue before resuming the download. - Images per page - - - - Interval (thumbnail) - + Interval (image) @@ -3753,295 +3686,225 @@ Please solve the issue before resuming the download. - + Interval (details) - + Interval (error) - - + + + + - - s - + Sources - + Source 1 - - - - + + + + XML - - - - + + + + JSON - - - - + + + + Regex - - - - + + + + RSS - + Source 2 - + Source 3 - + Source 4 - + Use default sources - - Credentials - - - - - + Username - - + Password - - Hash password - - - - - + Test - + Login - + Type - + Through URL - + GET - + POST - + OAuth 1 - + OAuth 2 - - Username field - - - - - Password field - - - - - Page limit - - - - - - URL - - - - - - Cookie - - - - - Request token url - - - - - Authorize url - - - - - Access token url - - - - - Request url - - - - - Token url - - - - - Refresh token url - - - - - Scope - - - - + Cookies - - + + Value - - + + Add - + Headers - + Delete - + Cancel - + Confirm - - Hash a password + + API key - - Please enter your password below.<br/>It will then be hashed using the format "%1". + + Consumer key - + + Consumer secret + + + + Delete a site - + Are you sure you want to delete the site %1? - + Connection... - + Success! - + Failure - + Unable to test - + Error - + You should at least select one source @@ -4079,33 +3942,33 @@ Please solve the issue before resuming the download. - + Options - + An update for this source is available. - + - No preset selected - - + Create a new preset - - + + Name - + Edit preset @@ -4163,12 +4026,12 @@ Please solve the issue before resuming the download. - + Choose a save folder - + An error occurred creating the save folder. @@ -4176,17 +4039,17 @@ Please solve the issue before resuming the download. TagContextMenu - + Remove from favorites - + Choose as image - + Add to favorites @@ -4201,47 +4064,47 @@ Please solve the issue before resuming the download. - + Don't blacklist - + Blacklist - + Don't ignore - + Ignore - + Copy tag - + Copy all tags - + Open in a new tab - + Open in new a window - + Open in browser @@ -4279,12 +4142,12 @@ Please solve the issue before resuming the download. - + Finished - + %n tag(s) loaded @@ -4294,77 +4157,72 @@ Please solve the issue before resuming the download. TagTab - - New tab - - - - + Pl&us - + O&k - + Maybe you meant: - + Post-filtering - + How many sources should appear per line. - + Number of columns - + Images per page - + Load more results - + S&ources - + &Merge results - + Get &selected - + Get this &page - + Get &all - + Search @@ -4378,47 +4236,115 @@ Please solve the issue before resuming the download. - + Remove - + Add - + Kept for later - + Ratings - + Sortings - + Copy - + Cut - + Paste + + TokenSettingsWidget + + + Form + + + + + If empty + + + + + Separator + + + + + Sort + + + + + Original + + + + + Name + + + + + If more than n tags + + + + + Keep all tags + + + + + Keep n tags + + + + + Keep n tags, then add + + + + + Replace all tags by + + + + + One file per tag + + + + + Use shortest if possible + + + UpdateDialog @@ -4437,7 +4363,7 @@ Please solve the issue before resuming the download. - + Version <b>%1</b> @@ -4464,14 +4390,14 @@ Please solve the issue before resuming the download. ZoomWindow - - + + Image - + Save @@ -4482,7 +4408,7 @@ Please solve the issue before resuming the download. - + Save and close @@ -4498,13 +4424,13 @@ Please solve the issue before resuming the download. - + Save (fav) - + Save and close (fav) @@ -4514,157 +4440,157 @@ Please solve the issue before resuming the download. - + Reload - + Copy file - + Copy data - + Folder does not exist - + The save folder does not exist yet. Create it? - + Error creating folder. %1 - - + + Saving... (fav) - - + + Saving... - + Saved! (fav) - + Saved! - + Copied! (fav) - + Copied! - + Moved! (fav) - + Moved! - + Link created! (fav) - + Link created! - + MD5 already exists (fav) - + MD5 already exists - + Already exists (fav) - + Already exists - + Delete (fav) - + Delete - + Close (fav) - + Close - + File is too big to be displayed. %1 - - + + Error - + You did not specified a save folder! Do you want to open the options window? - + You did not specified a save format! Do you want to open the options window? - + Error saving image. - + Save image diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index cadcf250c..777c245b8 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -17,6 +17,5 @@ target_link_libraries(${PROJECT_NAME} ${QT_LIBRARIES} ${LIBS}) # Pre-compiled header if(USE_PCH) - include(PrecompiledHeader) - add_precompiled_header(${PROJECT_NAME} "src/pch.h" FORCEINCLUDE SOURCE_CXX "${CMAKE_CURRENT_SOURCE_DIR}/src/pch.cpp") + cotire(${PROJECT_NAME}) endif() diff --git a/lib/src/analytics.cpp b/lib/src/analytics.cpp new file mode 100644 index 000000000..b6fec0178 --- /dev/null +++ b/lib/src/analytics.cpp @@ -0,0 +1,33 @@ +#include "analytics.h" + + +void Analytics::setTrackingID(QString trackingId) +{ + m_googleAnalytics.setTrackingID(trackingId); +} + +void Analytics::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + + +void Analytics::sendScreenView(QString screenName, QVariantMap customValues) +{ + if (!m_enabled) { + return; + } + + m_googleAnalytics.sendScreenView(std::move(screenName), std::move(customValues)); + m_googleAnalytics.startSending(); +} + +void Analytics::sendEvent(QString category, QString action, QString label, QVariant value, QVariantMap customValues) +{ + if (!m_enabled) { + return; + } + + m_googleAnalytics.sendEvent(std::move(category), std::move(action), std::move(label), std::move(value), std::move(customValues)); + m_googleAnalytics.startSending(); +} diff --git a/lib/src/analytics.h b/lib/src/analytics.h new file mode 100644 index 000000000..e9279fee7 --- /dev/null +++ b/lib/src/analytics.h @@ -0,0 +1,37 @@ +#ifndef ANALYTICS_H +#define ANALYTICS_H + +#include +#include +#include +#include "vendor/ganalytics.h" + + +class Analytics +{ + public: + // Singleton pattern + static Analytics &getInstance() + { + static Analytics instance; + return instance; + } + Analytics(Analytics const &) = delete; + void operator=(Analytics const &) = delete; + + // Setters + void setTrackingID(QString trackingId); + void setEnabled(bool enabled); + + // API + void sendScreenView(QString screenName, QVariantMap customValues = {}); + void sendEvent(QString category, QString action, QString label = {}, QVariant value = {}, QVariantMap customValues = {}); + + private: + Analytics() = default; + + bool m_enabled = false; + GAnalytics m_googleAnalytics; +}; + +#endif // ANALYTICS_H diff --git a/lib/src/auth/auth-const-field.cpp b/lib/src/auth/auth-const-field.cpp new file mode 100644 index 000000000..bc0d746ff --- /dev/null +++ b/lib/src/auth/auth-const-field.cpp @@ -0,0 +1,15 @@ +#include "auth/auth-const-field.h" +#include "mixed-settings.h" + + +AuthConstField::AuthConstField(QString key, QString value) + : AuthField(QString(), std::move(key), AuthField::Const), m_value(std::move(value)) +{} + + +QString AuthConstField::value(MixedSettings *settings) const +{ + Q_UNUSED(settings); + + return m_value; +} diff --git a/lib/src/auth/auth-const-field.h b/lib/src/auth/auth-const-field.h new file mode 100644 index 000000000..12b6c9327 --- /dev/null +++ b/lib/src/auth/auth-const-field.h @@ -0,0 +1,20 @@ +#ifndef AUTH_CONST_FIELD_H +#define AUTH_CONST_FIELD_H + +#include +#include "auth/auth-field.h" + + +class MixedSettings; + +class AuthConstField : public AuthField +{ + public: + AuthConstField(QString key, QString value); + QString value(MixedSettings *settings) const override; + + private: + QString m_value; +}; + +#endif // AUTH_CONST_FIELD_H diff --git a/lib/src/auth/auth-field.cpp b/lib/src/auth/auth-field.cpp new file mode 100644 index 000000000..ac99c3a30 --- /dev/null +++ b/lib/src/auth/auth-field.cpp @@ -0,0 +1,29 @@ +#include "auth/auth-field.h" +#include "mixed-settings.h" + + +AuthField::AuthField(QString id, QString key, FieldType type) + : m_id(std::move(id)), m_key(std::move(key)), m_type(type) +{} + + +QString AuthField::id() const +{ + return m_id; +} + +QString AuthField::key() const +{ + return m_key; +} + +AuthField::FieldType AuthField::type() const +{ + return m_type; +} + + +QString AuthField::value(MixedSettings *settings) const +{ + return settings->value("auth/" + m_id).toString(); +} diff --git a/lib/src/auth/auth-field.h b/lib/src/auth/auth-field.h new file mode 100644 index 000000000..253b650b0 --- /dev/null +++ b/lib/src/auth/auth-field.h @@ -0,0 +1,35 @@ +#ifndef AUTH_FIELD_H +#define AUTH_FIELD_H + +#include + + +class MixedSettings; + +class AuthField +{ + public: + enum FieldType + { + Text, + Password, + Hash, + Const + }; + + AuthField(QString id, QString key, FieldType type); + virtual ~AuthField() = default; + + QString id() const; + QString key() const; + FieldType type() const; + + virtual QString value(MixedSettings *settings) const; + + private: + QString m_id; + QString m_key; + FieldType m_type; +}; + +#endif // AUTH_FIELD_H diff --git a/lib/src/auth/auth-hash-field.cpp b/lib/src/auth/auth-hash-field.cpp new file mode 100644 index 000000000..19c4666ca --- /dev/null +++ b/lib/src/auth/auth-hash-field.cpp @@ -0,0 +1,35 @@ +#include "auth/auth-hash-field.h" +#include "mixed-settings.h" + + +AuthHashField::AuthHashField(QString key, QCryptographicHash::Algorithm algo, QString salt) + : AuthField(QString(), std::move(key), AuthField::Hash), m_algo(algo), m_salt(std::move(salt)) +{} + + +QString AuthHashField::value(MixedSettings *settings) const +{ + const QString username = settings->value("auth/pseudo").toString(); + const QString password = settings->value("auth/password").toString(); + + // Don't hash passwords twice + // FIXME: very long passwords won't get hashed + if (password.length() >= 32) { + return password; + } + + QString data = password; + if (!m_salt.isEmpty()) { + data = QString(m_salt); + data.replace("%pseudo%", username); + data.replace("%pseudo:lower%", username.toLower()); + data.replace("%password%", password); + } + + return QCryptographicHash::hash(data.toUtf8(), m_algo).toHex(); +} + +QString AuthHashField::salt() const +{ + return m_salt; +} diff --git a/lib/src/auth/auth-hash-field.h b/lib/src/auth/auth-hash-field.h new file mode 100644 index 000000000..daea33c25 --- /dev/null +++ b/lib/src/auth/auth-hash-field.h @@ -0,0 +1,23 @@ +#ifndef AUTH_HASH_FIELD_H +#define AUTH_HASH_FIELD_H + +#include +#include +#include "auth/auth-field.h" + + +class MixedSettings; + +class AuthHashField : public AuthField +{ + public: + AuthHashField(QString key, QCryptographicHash::Algorithm algo, QString salt); + QString value(MixedSettings *settings) const override; + QString salt() const; + + private: + QCryptographicHash::Algorithm m_algo; + QString m_salt; +}; + +#endif // AUTH_HASH_FIELD_H diff --git a/lib/src/auth/auth.cpp b/lib/src/auth/auth.cpp new file mode 100644 index 000000000..568e507ee --- /dev/null +++ b/lib/src/auth/auth.cpp @@ -0,0 +1,12 @@ +#include "auth/auth.h" + + +Auth::Auth(QString type) + : m_type(std::move(type)) +{} + + +QString Auth::type() const +{ + return m_type; +} diff --git a/lib/src/auth/auth.h b/lib/src/auth/auth.h new file mode 100644 index 000000000..5a9b662ea --- /dev/null +++ b/lib/src/auth/auth.h @@ -0,0 +1,18 @@ +#ifndef AUTH_H +#define AUTH_H + +#include + + +class Auth +{ + public: + explicit Auth(QString type); + virtual ~Auth() = default; + QString type() const; + + private: + QString m_type; +}; + +#endif // AUTH_H diff --git a/lib/src/auth/field-auth.cpp b/lib/src/auth/field-auth.cpp new file mode 100644 index 000000000..ac137bff5 --- /dev/null +++ b/lib/src/auth/field-auth.cpp @@ -0,0 +1,12 @@ +#include "auth/field-auth.h" + + +FieldAuth::FieldAuth(QString type, QList fields) + : Auth(std::move(type)), m_fields(std::move(fields)) +{} + + +QList FieldAuth::fields() const +{ + return m_fields; +} diff --git a/lib/src/auth/field-auth.h b/lib/src/auth/field-auth.h new file mode 100644 index 000000000..619c8dbe1 --- /dev/null +++ b/lib/src/auth/field-auth.h @@ -0,0 +1,21 @@ +#ifndef FIELD_AUTH_H +#define FIELD_AUTH_H + +#include +#include +#include "auth/auth.h" + + +class AuthField; + +class FieldAuth : public Auth +{ + public: + FieldAuth(QString type, QList fields); + QList fields() const; + + private: + QList m_fields; +}; + +#endif // FIELD_AUTH_H diff --git a/lib/src/auth/http-auth.cpp b/lib/src/auth/http-auth.cpp new file mode 100644 index 000000000..3d20f0c45 --- /dev/null +++ b/lib/src/auth/http-auth.cpp @@ -0,0 +1,17 @@ +#include "auth/http-auth.h" + + +HttpAuth::HttpAuth(QString type, QString url, QList fields, QString cookie) + : FieldAuth(std::move(type), std::move(fields)), m_url(std::move(url)), m_cookie(std::move(cookie)) +{} + + +QString HttpAuth::url() const +{ + return m_url; +} + +QString HttpAuth::cookie() const +{ + return m_cookie; +} diff --git a/lib/src/auth/http-auth.h b/lib/src/auth/http-auth.h new file mode 100644 index 000000000..682b13d40 --- /dev/null +++ b/lib/src/auth/http-auth.h @@ -0,0 +1,23 @@ +#ifndef HTTP_AUTH_H +#define HTTP_AUTH_H + +#include +#include +#include "auth/field-auth.h" + + +class AuthField; + +class HttpAuth : public FieldAuth +{ + public: + HttpAuth(QString type, QString url, QList fields, QString cookie); + QString url() const; + QString cookie() const; + + private: + QString m_url; + QString m_cookie; +}; + +#endif // HTTP_AUTH_H diff --git a/lib/src/auth/oauth2-auth.cpp b/lib/src/auth/oauth2-auth.cpp new file mode 100644 index 000000000..1dd7c55c5 --- /dev/null +++ b/lib/src/auth/oauth2-auth.cpp @@ -0,0 +1,17 @@ +#include "auth/oauth2-auth.h" + + +OAuth2Auth::OAuth2Auth(QString type, QString authType, QString tokenUrl) + : Auth(std::move(type)), m_authType(std::move(authType)), m_tokenUrl(std::move(tokenUrl)) +{} + + +QString OAuth2Auth::authType() const +{ + return m_authType; +} + +QString OAuth2Auth::tokenUrl() const +{ + return m_tokenUrl; +} diff --git a/lib/src/auth/oauth2-auth.h b/lib/src/auth/oauth2-auth.h new file mode 100644 index 000000000..ea2cadbd8 --- /dev/null +++ b/lib/src/auth/oauth2-auth.h @@ -0,0 +1,20 @@ +#ifndef OAUTH2_AUTH_H +#define OAUTH2_AUTH_H + +#include +#include "auth/auth.h" + + +class OAuth2Auth : public Auth +{ + public: + OAuth2Auth(QString type, QString authType, QString tokenUrl); + QString authType() const; + QString tokenUrl() const; + + private: + QString m_authType; + QString m_tokenUrl; +}; + +#endif // OAUTH2_AUTH_H diff --git a/lib/src/auth/url-auth.cpp b/lib/src/auth/url-auth.cpp new file mode 100644 index 000000000..6e638a675 --- /dev/null +++ b/lib/src/auth/url-auth.cpp @@ -0,0 +1,12 @@ +#include "auth/url-auth.h" + + +UrlAuth::UrlAuth(QString type, QList fields, int maxPage) + : FieldAuth(std::move(type), std::move(fields)), m_maxPage(maxPage) +{} + + +int UrlAuth::maxPage() const +{ + return m_maxPage; +} diff --git a/lib/src/auth/url-auth.h b/lib/src/auth/url-auth.h new file mode 100644 index 000000000..40f9073a5 --- /dev/null +++ b/lib/src/auth/url-auth.h @@ -0,0 +1,21 @@ +#ifndef URL_AUTH_H +#define URL_AUTH_H + +#include +#include +#include "auth/field-auth.h" + + +class AuthField; + +class UrlAuth : public FieldAuth +{ + public: + UrlAuth(QString type, QList fields, int maxPage); + int maxPage() const; + + private: + int m_maxPage; +}; + +#endif // URL_AUTH_H diff --git a/lib/src/backports/backports.h b/lib/src/backports/backports.h new file mode 100644 index 000000000..6f1a05fca --- /dev/null +++ b/lib/src/backports/backports.h @@ -0,0 +1,12 @@ +#ifndef BACKPORTS_H +#define BACKPORTS_H + +#include + + +#if (QT_VERSION < QT_VERSION_CHECK(5, 7, 0)) + #include "qasconst.h" + #include "qoverload.h" +#endif + +#endif // BACKPORTS_H diff --git a/lib/src/backports/qasconst.h b/lib/src/backports/qasconst.h new file mode 100644 index 000000000..9458120b9 --- /dev/null +++ b/lib/src/backports/qasconst.h @@ -0,0 +1,13 @@ +#ifndef BACKPORTS_QASCONST_H +#define BACKPORTS_QASCONST_H + +#include + + +template +Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } + +template +void qAsConst(const T &&) Q_DECL_EQ_DELETE; + +#endif // BACKPORTS_QASCONST_H diff --git a/lib/src/backports/qoverload.h b/lib/src/backports/qoverload.h new file mode 100644 index 000000000..ec6375250 --- /dev/null +++ b/lib/src/backports/qoverload.h @@ -0,0 +1,54 @@ +#ifndef BACKPORTS_QOVERLOAD_H +#define BACKPORTS_QOVERLOAD_H + +#include + + +template +struct QNonConstOverload +{ + template + Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } + + template + static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } +}; + +template +struct QConstOverload +{ + template + Q_DECL_CONSTEXPR auto operator()(R (T::*ptr)(Args...) const) const Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } + + template + static Q_DECL_CONSTEXPR auto of(R (T::*ptr)(Args...) const) Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } +}; + +template +struct QOverload : QConstOverload, QNonConstOverload +{ + using QConstOverload::of; + using QConstOverload::operator(); + using QNonConstOverload::of; + using QNonConstOverload::operator(); + + template + Q_DECL_CONSTEXPR auto operator()(R (*ptr)(Args...)) const Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } + + template + static Q_DECL_CONSTEXPR auto of(R (*ptr)(Args...)) Q_DECL_NOTHROW -> decltype(ptr) + { return ptr; } +}; + +#if defined(__cpp_variable_templates) && __cpp_variable_templates >= 201304 // C++14 + template Q_DECL_CONSTEXPR QOverload qOverload Q_DECL_UNUSED = {}; + template Q_DECL_CONSTEXPR QConstOverload qConstOverload Q_DECL_UNUSED = {}; + template Q_DECL_CONSTEXPR QNonConstOverload qNonConstOverload Q_DECL_UNUSED = {}; +#endif + +#endif // BACKPORTS_QOVERLOAD_H diff --git a/lib/src/commands/commands.cpp b/lib/src/commands/commands.cpp index 6fddc289f..55b7f6b16 100644 --- a/lib/src/commands/commands.cpp +++ b/lib/src/commands/commands.cpp @@ -48,8 +48,9 @@ bool Commands::start() const bool Commands::before() const { - if (!m_mysqlSettings.before.isEmpty()) + if (!m_mysqlSettings.before.isEmpty()) { return sqlExec(m_mysqlSettings.before); + } return true; } @@ -57,13 +58,11 @@ bool Commands::before() const bool Commands::image(const Image &img, const QString &path) { // Normal commands - if (!m_commandImage.isEmpty()) - { + if (!m_commandImage.isEmpty()) { Filename fn(m_commandImage); - QStringList execs = fn.path(img, m_profile, QString(), 0, false, false, false, false); + QStringList execs = fn.path(img, m_profile, QString(), 0, Filename::None); - for (QString exec : execs) - { + for (QString exec : execs) { exec.replace("%path:nobackslash%", QDir::toNativeSeparators(path).replace("\\", "/")) .replace("%path%", QDir::toNativeSeparators(path)); @@ -71,25 +70,25 @@ bool Commands::image(const Image &img, const QString &path) Logger::getInstance().logCommand(exec); const int code = QProcess::execute(exec); - if (code != 0) + if (code != 0) { log(QStringLiteral("Error executing command (return code: %1)").arg(code)); + } } } // SQL commands - if (!m_mysqlSettings.image.isEmpty()) - { + if (!m_mysqlSettings.image.isEmpty()) { Filename fn(m_mysqlSettings.image); fn.setEscapeMethod(&SqlWorker::escape); - QStringList execs = fn.path(img, m_profile, QString(), 0, false, false, false, false); + QStringList execs = fn.path(img, m_profile, QString(), 0, Filename::None); - for (QString exec : execs) - { + for (QString exec : execs) { exec.replace("%path:nobackslash%", m_sqlWorker->escape(QDir::toNativeSeparators(path).replace("\\", "/"))) .replace("%path%", m_sqlWorker->escape(QDir::toNativeSeparators(path))); - if (!sqlExec(exec)) + if (!sqlExec(exec)) { return false; + } } } @@ -101,14 +100,12 @@ bool Commands::tag(const Image &img, const Tag &tag, bool after) const QString original = QString(tag.text()).replace(" ", "_"); QString command = after ? m_commandTagAfter : m_commandTagBefore; - if (!command.isEmpty()) - { + if (!command.isEmpty()) { Filename fn(command); fn.setEscapeMethod(&SqlWorker::escape); - QStringList execs = fn.path(img, m_profile, QString(), 0, false, false, false, false, true); + QStringList execs = fn.path(img, m_profile, QString(), 0, Filename::KeepInvalidTokens); - for (QString exec : execs) - { + for (QString exec : execs) { exec.replace("%tag%", original) .replace("%original%", tag.text()) .replace("%type%", tag.type().name()) @@ -118,28 +115,28 @@ bool Commands::tag(const Image &img, const Tag &tag, bool after) Logger::getInstance().logCommand(exec); const int code = QProcess::execute(exec); - if (code != 0) + if (code != 0) { log(QStringLiteral("Error executing command (return code: %1)").arg(code)); + } } } QString commandSql = after ? m_mysqlSettings.tagAfter : m_mysqlSettings.tagBefore; - if (!commandSql.isEmpty()) - { + if (!commandSql.isEmpty()) { start(); Filename fn(commandSql); - QStringList execs = fn.path(img, m_profile, QString(), 0, false, false, false, false, true); + QStringList execs = fn.path(img, m_profile, QString(), 0, Filename::KeepInvalidTokens); - for (QString exec : execs) - { + for (QString exec : execs) { exec.replace("%tag%", m_sqlWorker->escape(original)) .replace("%original%", m_sqlWorker->escape(tag.text())) .replace("%type%", m_sqlWorker->escape(tag.type().name())) .replace("%number%", QString::number(tag.type().number())); - if (!sqlExec(exec)) + if (!sqlExec(exec)) { return false; + } } } @@ -148,8 +145,9 @@ bool Commands::tag(const Image &img, const Tag &tag, bool after) bool Commands::after() const { - if (!m_mysqlSettings.after.isEmpty()) + if (!m_mysqlSettings.after.isEmpty()) { return sqlExec(m_mysqlSettings.after); + } return true; } diff --git a/lib/src/commands/sql-worker.cpp b/lib/src/commands/sql-worker.cpp index a27a63416..1410564a8 100644 --- a/lib/src/commands/sql-worker.cpp +++ b/lib/src/commands/sql-worker.cpp @@ -18,8 +18,9 @@ SqlWorker::SqlWorker(QString driver, QString host, QString user, QString passwor bool SqlWorker::connect() { - if (!m_enabled || m_started) + if (!m_enabled || m_started) { return true; + } QSqlDatabase db = QSqlDatabase::addDatabase(m_driver); db.setDatabaseName(m_database); @@ -27,16 +28,14 @@ bool SqlWorker::connect() db.setPassword(m_password); const int portSeparator = m_host.lastIndexOf(':'); - if (portSeparator > 0) - { + if (portSeparator > 0) { db.setHostName(m_host.left(portSeparator)); db.setPort(m_host.midRef(portSeparator + 1).toInt()); + } else { + db.setHostName(m_host); } - else - { db.setHostName(m_host); } - if (!db.open()) - { + if (!db.open()) { log(QStringLiteral("Error initializing commands: %1").arg(db.lastError().text()), Logger::Error); return false; } @@ -48,8 +47,9 @@ bool SqlWorker::connect() QString SqlWorker::escape(const QVariant &val) { QSqlDriver *driver = QSqlDatabase::database().driver(); - if (driver == nullptr) + if (driver == nullptr) { return nullptr; + } QSqlField f; f.setType(val.type()); @@ -60,8 +60,9 @@ QString SqlWorker::escape(const QVariant &val) bool SqlWorker::execute(const QString &sql) { - if (!m_enabled || !connect()) + if (!m_enabled || !connect()) { return false; + } log(QStringLiteral("SQL execution of \"%1\"").arg(sql)); Logger::getInstance().logCommandSql(sql); diff --git a/lib/src/custom-network-access-manager.cpp b/lib/src/custom-network-access-manager.cpp index 20daeff6c..3bf0340c6 100644 --- a/lib/src/custom-network-access-manager.cpp +++ b/lib/src/custom-network-access-manager.cpp @@ -1,7 +1,10 @@ #include "custom-network-access-manager.h" #include +#include +#include #include #include "functions.h" +#include "logger.h" #include "vendor/qcustomnetworkreply.h" QQueue CustomNetworkAccessManager::NextFiles; @@ -13,82 +16,98 @@ CustomNetworkAccessManager::CustomNetworkAccessManager(QObject *parent) connect(this, &QNetworkAccessManager::sslErrors, this, &CustomNetworkAccessManager::sslErrorHandler); } -QNetworkReply *CustomNetworkAccessManager::get(const QNetworkRequest &request) +QNetworkReply *CustomNetworkAccessManager::makeTestReply(const QNetworkRequest &request) { - if (isTestModeEnabled()) - { - QString md5 = QString(QCryptographicHash::hash(request.url().toString().toLatin1(), QCryptographicHash::Md5).toHex()); - const QString filename = request.url().fileName(); - const QString ext = filename.contains('.') ? filename.mid(filename.lastIndexOf('.') + 1) : QStringLiteral("html"); - const QString host = request.url().host(); - QString path = "tests/resources/pages/" + host + "/" + md5 + "." + ext; - - const bool fromQueue = !CustomNetworkAccessManager::NextFiles.isEmpty(); - if (fromQueue) - { path = CustomNetworkAccessManager::NextFiles.dequeue(); } - - // Error testing - if (path == QLatin1String("404") || path == QLatin1String("500")) - { - auto *reply = new QCustomNetworkReply(this); - if (path == QLatin1String("404")) - { - reply->setHttpStatusCode(404, "Not Found"); - reply->setNetworkError(QNetworkReply::ContentNotFoundError, QStringLiteral("Not Found")); - } - else - { - reply->setHttpStatusCode(500, "Internal Server Error"); - reply->setNetworkError(QNetworkReply::UnknownNetworkError, QStringLiteral("Internal Server Error")); - } - reply->setContentType("text/html"); - reply->setContent(QByteArray()); - return reply; + QString md5 = QString(QCryptographicHash::hash(request.url().toString().toLatin1(), QCryptographicHash::Md5).toHex()); + const QString filename = request.url().fileName(); + const QString ext = filename.contains('.') ? filename.mid(filename.lastIndexOf('.') + 1) : QStringLiteral("html"); + const QString host = request.url().host(); + QString path = "tests/resources/pages/" + host + "/" + md5 + "." + ext; + + const bool fromQueue = !CustomNetworkAccessManager::NextFiles.isEmpty(); + if (fromQueue) { + path = CustomNetworkAccessManager::NextFiles.dequeue(); + } + + // Error testing + if (path == QLatin1String("404") || path == QLatin1String("500") || path == QLatin1String("cookie") || path == QLatin1String("redirect")) { + auto *reply = new QCustomNetworkReply(this); + if (path == QLatin1String("404")) { + reply->setHttpStatusCode(404, "Not Found"); + reply->setNetworkError(QNetworkReply::ContentNotFoundError, QStringLiteral("Not Found")); + } else if (path == QLatin1String("cookie")) { + cookieJar()->insertCookie(QNetworkCookie("test_cookie", "test_value")); + reply->setHttpStatusCode(200, "OK"); + } else if (path == QLatin1String("redirect")) { + reply->setAttribute(QNetworkRequest::RedirectionTargetAttribute, QUrl("https://www.test-redirect.com")); + reply->setHttpStatusCode(200, "OK"); + } else { + reply->setHttpStatusCode(500, "Internal Server Error"); + reply->setNetworkError(QNetworkReply::UnknownNetworkError, QStringLiteral("Internal Server Error")); } + reply->setContentType("text/html"); + reply->setContent(QByteArray()); + return reply; + } + + QFile f(path); + const bool opened = f.open(QFile::ReadOnly); + const bool logFilename = !opened || !fromQueue; + if (!opened) { + md5 = QString(QCryptographicHash::hash(request.url().toString().toLatin1(), QCryptographicHash::Md5).toHex()); + f.setFileName("tests/resources/pages/" + host + "/" + md5 + "." + ext); + + if (!f.open(QFile::ReadOnly)) { + // LCOV_EXCL_START + if (ext != QLatin1String("jpg") && ext != QLatin1String("png")) { + qDebug() << ("Test file not found: " + f.fileName() + " (" + request.url().toString() + ")"); + return nullptr; + } + // LCOV_EXCL_STOP + + f.setFileName(QStringLiteral("tests/resources/image_1x1.png")); - QFile f(path); - const bool opened = f.open(QFile::ReadOnly); - const bool logFilename = !opened || !fromQueue; - if (!opened) - { - md5 = QString(QCryptographicHash::hash(request.url().toString().toLatin1(), QCryptographicHash::Md5).toHex()); - f.setFileName("tests/resources/pages/" + host + "/" + md5 + "." + ext); - - if (!f.open(QFile::ReadOnly)) - { - // LCOV_EXCL_START - if (ext != QLatin1String("jpg") && ext != QLatin1String("png")) - { - qDebug() << ("Test file not found: " + f.fileName() + " (" + request.url().toString() + ")"); - return nullptr; - } - // LCOV_EXCL_STOP - - f.setFileName(QStringLiteral("tests/resources/image_1x1.png")); - - // LCOV_EXCL_START - if (!f.open(QFile::ReadOnly)) - return nullptr; - // LCOV_EXCL_STOP + // LCOV_EXCL_START + if (!f.open(QFile::ReadOnly)) { + return nullptr; } + // LCOV_EXCL_STOP } + } - if (logFilename) - { qDebug() << ("Reply from file: " + request.url().toString() + " -> " + f.fileName()); } - const QByteArray content = f.readAll(); + if (logFilename) { + qDebug() << ("Reply from file: " + request.url().toString() + " -> " + f.fileName()); + } + const QByteArray content = f.readAll(); - auto *reply = new QCustomNetworkReply(this); - reply->setHttpStatusCode(200, "OK"); - reply->setContentType("text/html"); - reply->setContent(content); + auto *reply = new QCustomNetworkReply(this); + reply->setHttpStatusCode(200, "OK"); + reply->setContentType("text/html"); + reply->setContent(content); - return reply; + return reply; +} + +QNetworkReply *CustomNetworkAccessManager::get(const QNetworkRequest &request) +{ + if (isTestModeEnabled()) { + return makeTestReply(request); } log(QStringLiteral("Loading `%1`").arg(request.url().toString().toHtmlEscaped()), Logger::Debug); return QNetworkAccessManager::get(request); } +QNetworkReply *CustomNetworkAccessManager::post(const QNetworkRequest &request, const QByteArray &data) +{ + if (isTestModeEnabled()) { + return makeTestReply(request); + } + + log(QStringLiteral("Posting to `%1`").arg(request.url().toString().toHtmlEscaped()), Logger::Debug); + return QNetworkAccessManager::post(request, data); +} + /** * Log SSL errors in debug mode only. * diff --git a/lib/src/custom-network-access-manager.h b/lib/src/custom-network-access-manager.h index fc691e6e1..92a744c64 100644 --- a/lib/src/custom-network-access-manager.h +++ b/lib/src/custom-network-access-manager.h @@ -17,9 +17,13 @@ class CustomNetworkAccessManager : public QNetworkAccessManager public: explicit CustomNetworkAccessManager(QObject *parent = nullptr); QNetworkReply *get(const QNetworkRequest &request); + QNetworkReply *post(const QNetworkRequest &request, const QByteArray &data); void sslErrorHandler(QNetworkReply *reply, const QList &errors); static QQueue NextFiles; + + protected: + QNetworkReply *makeTestReply(const QNetworkRequest &request); }; #endif // CUSTOMNETWORKACCESSMANAGER_H diff --git a/lib/src/danbooru-downloader-importer.cpp b/lib/src/danbooru-downloader-importer.cpp index 429244c18..466217033 100644 --- a/lib/src/danbooru-downloader-importer.cpp +++ b/lib/src/danbooru-downloader-importer.cpp @@ -10,8 +10,7 @@ DanbooruDownloaderImporter::DanbooruDownloaderImporter() { QSettings cfg(QSettings::IniFormat, QSettings::UserScope, "Mozilla", "Firefox"); const QString path = QFileInfo(cfg.fileName()).absolutePath() + "/Firefox"; - if (QFile::exists(path + "/profiles.ini")) - { + if (QFile::exists(path + "/profiles.ini")) { QSettings profiles(path + "/profiles.ini", QSettings::IniFormat); m_firefoxProfilePath = path + "/" + profiles.value("Profile0/Path").toString(); } @@ -25,8 +24,9 @@ bool DanbooruDownloaderImporter::isInstalled() const void DanbooruDownloaderImporter::import(QSettings *dest) const { QFile prefs(m_firefoxProfilePath + "/prefs.js"); - if (prefs.exists() && prefs.open(QIODevice::ReadOnly | QIODevice::Text)) + if (prefs.exists() && prefs.open(QIODevice::ReadOnly | QIODevice::Text)) { return; + } const QString source = prefs.readAll(); QRegularExpression rx("user_pref\\(\"danbooru.downloader.([^\"]+)\", ([^\\)]+)\\);"); @@ -49,22 +49,24 @@ void DanbooruDownloaderImporter::import(QSettings *dest) const assoc["targetName"] = "filename"; auto matches = rx.globalMatch(source); - while (matches.hasNext()) - { + while (matches.hasNext()) { auto match = matches.next(); QString value = match.captured(2); - if (value.startsWith('"')) { value = value.right(value.length() - 1); } - if (value.endsWith('"')) { value = value.left(value.length() - 1); } + if (value.startsWith('"')) { + value = value.right(value.length() - 1); + } + if (value.endsWith('"')) { + value = value.left(value.length() - 1); + } firefox[match.captured(1)] = value; } dest->beginGroup("Save"); - if (firefox.contains("useBlacklist")) - { dest->setValue("downloadblacklist", firefox["useBlacklist"] != "true"); } - for (auto it = firefox.constBegin(); it != firefox.constEnd(); ++it) - { - if (assoc.contains(it.key())) - { + if (firefox.contains("useBlacklist")) { + dest->setValue("downloadblacklist", firefox["useBlacklist"] != "true"); + } + for (auto it = firefox.constBegin(); it != firefox.constEnd(); ++it) { + if (assoc.contains(it.key())) { QString v(it.value()); v.replace("\\\\", "\\"); dest->setValue(assoc[it.key()], v); diff --git a/lib/src/downloader/download-query-group.cpp b/lib/src/downloader/download-query-group.cpp index dec5357d0..41539397c 100644 --- a/lib/src/downloader/download-query-group.cpp +++ b/lib/src/downloader/download-query-group.cpp @@ -4,67 +4,72 @@ #include "models/site.h" -DownloadQueryGroup::DownloadQueryGroup(QSettings *settings, QString tags, int page, int perPage, int total, QStringList postFiltering, Site *site, QString unk) - : DownloadQuery(site), tags(std::move(tags)), page(page), perpage(perPage), total(total), postFiltering(std::move(postFiltering)), unk(std::move(unk)) +DownloadQueryGroup::DownloadQueryGroup(QSettings *settings, SearchQuery query, int page, int perPage, int total, QStringList postFiltering, Site *site) + : DownloadQuery(site), query(std::move(query)), page(page), perpage(perPage), total(total), postFiltering(std::move(postFiltering)) { getBlacklisted = settings->value("downloadblacklist").toBool(); filename = settings->value("Save/filename").toString(); path = settings->value("Save/path").toString(); } -DownloadQueryGroup::DownloadQueryGroup(QString tags, int page, int perPage, int total, QStringList postFiltering, bool getBlacklisted, Site *site, const QString &filename, const QString &path, QString unk) - : DownloadQuery(site, filename, path), tags(std::move(tags)), page(page), perpage(perPage), total(total), postFiltering(std::move(postFiltering)), getBlacklisted(getBlacklisted), unk(std::move(unk)) -{ } +DownloadQueryGroup::DownloadQueryGroup(SearchQuery query, int page, int perPage, int total, QStringList postFiltering, bool getBlacklisted, Site *site, const QString &filename, const QString &path) + : DownloadQuery(site, filename, path), query(std::move(query)), page(page), perpage(perPage), total(total), postFiltering(std::move(postFiltering)), getBlacklisted(getBlacklisted) +{} void DownloadQueryGroup::write(QJsonObject &json) const { - json["tags"] = QJsonArray::fromStringList(tags.split(' ', QString::SkipEmptyParts)); + QJsonObject jsonQuery; + query.write(jsonQuery); + json["query"] = jsonQuery; + json["page"] = page; json["perpage"] = perpage; json["total"] = total; json["postFiltering"] = QJsonArray::fromStringList(postFiltering); json["getBlacklisted"] = getBlacklisted; + json["galleriesCountAsOne"] = galleriesCountAsOne; json["site"] = site->url(); json["filename"] = QString(filename).replace("\n", "\\n"); json["path"] = path; + + json["progressVal"] = progressVal; + json["progressFinished"] = progressFinished; } bool DownloadQueryGroup::read(const QJsonObject &json, const QMap &sites) { - QJsonArray jsonTags = json["tags"].toArray(); - QStringList tgs; - tgs.reserve(jsonTags.count()); - for (auto tag : jsonTags) - tgs.append(tag.toString()); + query.read(json.contains("query") ? json["query"].toObject() : json, sites); - tags = tgs.join(' '); page = json["page"].toInt(); perpage = json["perpage"].toInt(); total = json["total"].toInt(); getBlacklisted = json["getBlacklisted"].toBool(); + galleriesCountAsOne = json["galleriesCountAsOne"].toBool(); filename = json["filename"].toString().replace("\\n", "\n"); path = json["path"].toString(); + progressVal = json["progressVal"].toInt(); + progressFinished = json["progressFinished"].toBool(); + // Post filtering postFiltering.clear(); QJsonArray jsonPostFilters = json["postFiltering"].toArray(); - for (auto tag : jsonPostFilters) + for (auto tag : jsonPostFilters) { postFiltering.append(tag.toString()); + } // Get site const QString siteName = json["site"].toString(); - if (!sites.contains(siteName)) - { + if (!sites.contains(siteName)) { return false; } site = sites[siteName]; // Validate values - if (page < 1 || perpage < 1 || total < 1) - { + if (page < 1 || perpage < 1 || total < 1) { return false; } @@ -74,11 +79,12 @@ bool DownloadQueryGroup::read(const QJsonObject &json, const QMap #include #include "downloader/download-query.h" +#include "models/search-query/search-query.h" class QSettings; @@ -16,21 +17,23 @@ class DownloadQueryGroup : public DownloadQuery public: // Constructors DownloadQueryGroup() = default; - explicit DownloadQueryGroup(QSettings *settings, QString tags, int page, int perPage, int total, QStringList postFiltering, Site *site, QString unk = QString()); - explicit DownloadQueryGroup(QString tags, int page, int perPage, int total, QStringList postFiltering, bool getBlacklisted, Site *site, const QString &filename, const QString &path, QString unk = QString()); + explicit DownloadQueryGroup(QSettings *settings, SearchQuery query, int page, int perPage, int total, QStringList postFiltering, Site *site); + explicit DownloadQueryGroup(SearchQuery query, int page, int perPage, int total, QStringList postFiltering, bool getBlacklisted, Site *site, const QString &filename, const QString &path); // Serialization void write(QJsonObject &json) const override; bool read(const QJsonObject &json, const QMap &sites) override; // Public members - QString tags; + SearchQuery query; int page; int perpage; int total; QStringList postFiltering; bool getBlacklisted; - QString unk; + bool galleriesCountAsOne = true; + int progressVal = 0; + bool progressFinished = false; }; bool operator==(const DownloadQueryGroup &lhs, const DownloadQueryGroup &rhs); diff --git a/lib/src/downloader/download-query-image.cpp b/lib/src/downloader/download-query-image.cpp index 2ce436948..e5f7452b6 100644 --- a/lib/src/downloader/download-query-image.cpp +++ b/lib/src/downloader/download-query-image.cpp @@ -6,96 +6,47 @@ #include "tags/tag.h" -DownloadQueryImage::DownloadQueryImage(QSettings *settings, const Image &img, Site *site) - : DownloadQuery(site) +DownloadQueryImage::DownloadQueryImage(QSettings *settings, QSharedPointer img, Site *site) + : DownloadQuery(site), image(std::move(img)) { filename = settings->value("Save/filename").toString(); path = settings->value("Save/path").toString(); - - initFromImage(img); -} - -DownloadQueryImage::DownloadQueryImage(const Image &img, Site *site, const QString &filename, const QString &path) - : DownloadQuery(site, filename, path) -{ - initFromImage(img); -} - -DownloadQueryImage::DownloadQueryImage(qulonglong id, const QString &md5, const QString &rating, const QString &tags, const QString &fileUrl, const QString &date, Site *site, const QString &filename, const QString &path, const QStringList &search) - : DownloadQuery(site, filename, path) -{ - initFromData(id, md5, rating, tags, fileUrl, date, search); } -void DownloadQueryImage::initFromImage(const Image &img) -{ - const QList &imgTags = img.tags(); - - QStringList tags; - tags.reserve(imgTags.count()); - for (const Tag &tag : imgTags) - tags.append(tag.text()); - - initFromData(img.id(), img.md5(), img.rating(), tags.join(" "), img.fileUrl().toString(), img.createdAt().toString(Qt::ISODate), img.search()); -} - -void DownloadQueryImage::initFromData(qulonglong id, const QString &md5, const QString &rating, const QString &tags, const QString &fileUrl, const QString &date, const QStringList &search) -{ - values["filename"] = filename; - values["path"] = path; - values["site"] = site->name(); - - values["id"] = QString::number(id); - values["md5"] = md5; - values["rating"] = rating; - values["tags"] = tags; - values["date"] = date; - values["file_url"] = fileUrl; - values["search"] = search.join(' '); -} +DownloadQueryImage::DownloadQueryImage(QSharedPointer img, Site *site, const QString &filename, const QString &path) + : DownloadQuery(site, filename, path), image(std::move(img)) +{} void DownloadQueryImage::write(QJsonObject &json) const { - json["id"] = values["id"].toInt(); - json["md5"] = values["md5"]; - json["rating"] = values["rating"]; - json["tags"] = QJsonArray::fromStringList(values["tags"].split(' ', QString::SkipEmptyParts)); - json["file_url"] = values["file_url"]; - json["date"] = values["date"]; - json["search"] = values["search"]; - json["site"] = site->url(); json["filename"] = QString(filename).replace("\n", "\\n"); json["path"] = path; + + QJsonObject jsonImage; + image->write(jsonImage); + json["image"] = jsonImage; } bool DownloadQueryImage::read(const QJsonObject &json, const QMap &sites) { - QJsonArray jsonTags = json["tags"].toArray(); - QStringList tags; - tags.reserve(jsonTags.count()); - for (auto tag : jsonTags) - tags.append(tag.toString()); - - values["id"] = QString::number(json["id"].toInt()); - values["md5"] = json["md5"].toString(); - values["rating"] = json["rating"].toString(); - values["tags"] = tags.join(' '); - values["file_url"] = json["file_url"].toString(); - values["date"] = json["date"].toString(); - values["search"] = json["search"].toString(); - - filename = json["filename"].toString().replace("\\n", "\n"); - path = json["path"].toString(); - - // Get site const QString siteName = json["site"].toString(); - if (!sites.contains(siteName)) - { + if (!sites.contains(siteName)) { + return false; + } + + auto img = new Image(); + if (img->read(json["image"].toObject(), sites)) { + image = QSharedPointer(img); + } else { + img->deleteLater(); return false; } + site = sites[siteName]; + filename = json["filename"].toString().replace("\\n", "\n"); + path = json["path"].toString(); return true; } @@ -103,7 +54,7 @@ bool DownloadQueryImage::read(const QJsonObject &json, const QMap #include #include +#include #include #include #include "downloader/download-query.h" @@ -18,20 +19,15 @@ class DownloadQueryImage : public DownloadQuery public: // Constructors DownloadQueryImage() = default; - explicit DownloadQueryImage(QSettings *settings, const Image &img, Site *site); - explicit DownloadQueryImage(const Image &img, Site *site, const QString &filename, const QString &path); - explicit DownloadQueryImage(qulonglong id, const QString &md5, const QString &rating, const QString &tags, const QString &fileUrl, const QString &date, Site *site, const QString &filename, const QString &path, const QStringList &search); + explicit DownloadQueryImage(QSettings *settings, QSharedPointer img, Site *site); + explicit DownloadQueryImage(QSharedPointer img, Site *site, const QString &filename, const QString &path); // Serialization void write(QJsonObject &json) const override; bool read(const QJsonObject &json, const QMap &sites) override; // Public members - QMap values; - - private: - void initFromImage(const Image &img); - void initFromData(qulonglong id, const QString &md5, const QString &rating, const QString &tags, const QString &fileUrl, const QString &date, const QStringList &search); + QSharedPointer image; }; bool operator==(const DownloadQueryImage &lhs, const DownloadQueryImage &rhs); diff --git a/lib/src/downloader/download-query-loader.cpp b/lib/src/downloader/download-query-loader.cpp index 4a6f57d7d..c48fedfb9 100644 --- a/lib/src/downloader/download-query-loader.cpp +++ b/lib/src/downloader/download-query-loader.cpp @@ -10,8 +10,7 @@ bool DownloadQueryLoader::load(const QString &path, QList &uniques, QList &groups, const QMap &sites) { QFile f(path); - if (!f.open(QFile::ReadOnly)) - { + if (!f.open(QFile::ReadOnly)) { return false; } @@ -19,50 +18,9 @@ bool DownloadQueryLoader::load(const QString &path, QList &u QString header = f.readLine().trimmed(); // Version 1 and 2 are plain text - if (header.startsWith("[IGL ")) - { - const QChar fieldSeparator(29); - const QChar lineSeparator(28); - - // Read the remaining file - QString links = f.readAll(); - f.close(); - QStringList det = links.split(lineSeparator, QString::SkipEmptyParts); - if (det.empty()) - return false; - - for (const QString &link : det) - { - QStringList infos = link.split(fieldSeparator); - if (infos.size() == 9) - { - const QString &source = infos[6]; - if (!sites.contains(source)) - continue; - - uniques.append(DownloadQueryImage(infos[0].toULongLong(), infos[1], infos[2], infos[3], infos[4], infos[5], sites[source], infos[7], infos[8], QStringList())); - } - else - { - const QString &source = infos[5]; - if (!sites.contains(source) || infos.at(1).toInt() < 0 || infos.at(2).toInt() < 1 || infos.at(3).toInt() < 1) - continue; - - groups.append(DownloadQueryGroup( - infos[0], - infos[1].toInt(), - infos[2].toInt(), - infos[3].toInt(), - QStringList(), - infos[4] != QLatin1String("false"), - sites[source], - infos[6], - infos[7] - )); - } - } - - return true; + if (header.startsWith("[IGL ")) { + log(QStringLiteral("Text-based IGL files are not supported"), Logger::Warning); + return false; } // Other versions are JSON-based @@ -78,19 +36,19 @@ bool DownloadQueryLoader::load(const QString &path, QList &u case 3: { QJsonArray groupsJson = object["batchs"].toArray(); - for (auto groupJson : groupsJson) - { + for (auto groupJson : groupsJson) { DownloadQueryGroup batch; - if (batch.read(groupJson.toObject(), sites)) + if (batch.read(groupJson.toObject(), sites)) { groups.append(batch); + } } QJsonArray uniquesJson = object["uniques"].toArray(); - for (auto uniqueJson : uniquesJson) - { + for (auto uniqueJson : uniquesJson) { DownloadQueryImage unique; - if (unique.read(uniqueJson.toObject(), sites)) + if (unique.read(uniqueJson.toObject(), sites)) { uniques.append(unique); + } } return true; } @@ -104,15 +62,13 @@ bool DownloadQueryLoader::load(const QString &path, QList &u bool DownloadQueryLoader::save(const QString &path, const QList &uniques, const QList &groups) { QFile saveFile(path); - if (!saveFile.open(QFile::WriteOnly)) - { + if (!saveFile.open(QFile::WriteOnly)) { return false; } // Batch downloads QJsonArray groupsJson; - for (const auto &b : groups) - { + for (const auto &b : groups) { QJsonObject batch; b.write(batch); groupsJson.append(batch); @@ -120,8 +76,7 @@ bool DownloadQueryLoader::save(const QString &path, const QList &sites) = 0; diff --git a/lib/src/downloader/downloader.cpp b/lib/src/downloader/downloader.cpp index fcff09d38..849c67a22 100644 --- a/lib/src/downloader/downloader.cpp +++ b/lib/src/downloader/downloader.cpp @@ -4,9 +4,11 @@ #include "downloader/image-downloader.h" #include "functions.h" #include "logger.h" +#include "models/api/api.h" #include "models/page.h" #include "models/site.h" #include "tags/tag.h" +#include "tags/tag-api.h" Downloader::~Downloader() @@ -31,7 +33,7 @@ void Downloader::clear() Downloader::Downloader(Profile *profile, QStringList tags, QStringList postFiltering, QList sources, int page, int max, int perPage, QString location, QString filename, QString user, QString password, bool blacklist, Blacklist blacklistedTags, bool noDuplicates, int tagsMin, QString tagsFormat, Downloader *previous) : m_profile(profile), m_lastPage(nullptr), m_tags(std::move(tags)), m_postFiltering(std::move(postFiltering)), m_sites(std::move(sources)), m_page(page), m_max(max), m_perPage(perPage), m_waiting(0), m_ignored(0), m_duplicates(0), m_tagsMin(tagsMin), m_location(std::move(location)), m_filename(std::move(filename)), m_user(std::move(user)), m_password(std::move(password)), m_blacklist(blacklist), m_noDuplicates(noDuplicates), m_tagsFormat(std::move(tagsFormat)), m_blacklistedTags(std::move(blacklistedTags)), m_cancelled(false), m_quit(false), m_previous(previous) -{ } +{} void Downloader::setQuit(bool quit) { @@ -40,8 +42,7 @@ void Downloader::setQuit(bool quit) void Downloader::getPageCount() { - if (m_sites.empty()) - { + if (m_sites.empty()) { std::cerr << "No valid source found" << std::endl; return; } @@ -51,8 +52,7 @@ void Downloader::getPageCount() m_duplicates = 0; m_cancelled = false; - for (Site *site : qAsConst(m_sites)) - { + for (Site *site : qAsConst(m_sites)) { Page *page = new Page(m_profile, site, m_sites, m_tags, m_page, m_perPage, m_postFiltering, true, this); connect(page, &Page::finishedLoadingTags, this, &Downloader::finishedLoadingPageCount); @@ -65,31 +65,32 @@ void Downloader::getPageCount() } void Downloader::finishedLoadingPageCount(Page *page) { - if (m_cancelled) + if (m_cancelled) { return; + } log(QStringLiteral("Received page count '%1' (%2)").arg(page->url().toString(), QString::number(page->images().count()))); - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } int total = 0; - for (Page *p : qAsConst(m_pagesC)) + for (Page *p : qAsConst(m_pagesC)) { total += p->imagesCount(); + } - if (m_quit) + if (m_quit) { returnInt(total); - else + } else { emit finishedPageCount(total); + } } void Downloader::getPageTags() { - if (m_sites.empty()) - { + if (m_sites.empty()) { std::cerr << "No valid source found" << std::endl; return; } @@ -97,8 +98,7 @@ void Downloader::getPageTags() m_waiting = 0; m_cancelled = false; - for (Site *site : qAsConst(m_sites)) - { + for (Site *site : qAsConst(m_sites)) { Page *page = new Page(m_profile, site, m_sites, m_tags, m_page, m_perPage, m_postFiltering, true, this); connect(page, &Page::finishedLoadingTags, this, &Downloader::finishedLoadingPageTags); @@ -111,52 +111,51 @@ void Downloader::getPageTags() } void Downloader::finishedLoadingPageTags(Page *page) { - if (m_cancelled) + if (m_cancelled) { return; + } log(QStringLiteral("Received tags '%1' (%2)").arg(page->url().toString(), QString::number(page->tags().count()))); - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } QList list; - for (auto p : qAsConst(m_pagesT)) - { + for (auto p : qAsConst(m_pagesT)) { const QList &pageTags = p->tags(); - for (const Tag &tag : pageTags) - { + for (const Tag &tag : pageTags) { bool found = false; - for (auto &t : list) - { - if (t.text() == tag.text()) - { + for (auto &t : list) { + if (t.text() == tag.text()) { t.setCount(t.count() + tag.count()); found = true; } } - if (!found) + if (!found) { list.append(tag); + } } } QMutableListIterator i(list); - while (i.hasNext()) - if (i.next().count() < m_tagsMin) + while (i.hasNext()) { + if (i.next().count() < m_tagsMin) { i.remove(); + } + } - if (m_quit) + if (m_quit) { returnTagList(list); - else + } else { emit finishedTags(list); + } } void Downloader::getTags() { - if (m_sites.empty()) - { + if (m_sites.empty()) { std::cerr << "No valid source found" << std::endl; return; } @@ -164,14 +163,12 @@ void Downloader::getTags() m_waiting = 0; m_cancelled = false; - for (Site *site : qAsConst(m_sites)) - { + for (Site *site : qAsConst(m_sites)) { int pages = qCeil(static_cast(m_max) / m_perPage); - if (pages <= 0 || m_perPage <= 0 || m_max <= 0) + if (pages <= 0 || m_perPage <= 0 || m_max <= 0) { pages = 1; - connect(site, &Site::finishedLoadingTags, this, &Downloader::finishedLoadingTags); - for (int p = 0; p < pages; ++p) - { + } + for (int p = 0; p < pages; ++p) { m_pagesP.append(QPair(site, m_page + p)); m_oPagesP.append(QPair(site, m_page + p)); m_waiting++; @@ -180,92 +177,122 @@ void Downloader::getTags() loadNext(); } +Api *getTagApi(Site *site) +{ + for (Api *a : site->getApis()) { + if (a->canLoadTags()) { + return a; + } + } + return nullptr; +} void Downloader::loadNext() { - if (m_cancelled) + if (m_cancelled) { return; + } - if (!m_oPagesP.isEmpty()) - { - QPair tag = m_oPagesP.takeFirst(); + if (!m_oPagesP.isEmpty()) { log(QStringLiteral("Loading tags")); - tag.first->loadTags(tag.second, m_perPage); + QPair tag = m_oPagesP.takeFirst(); + Site *site = tag.first; + + Api *api = getTagApi(site); + if (api == nullptr) { + log(QStringLiteral("No valid API for loading tags for source: %1").arg(site->url()), Logger::Error); + return; + } + + auto *tagApi = new TagApi(m_profile, site, api, tag.second, m_perPage, this); + connect(tagApi, &TagApi::finishedLoading, this, &Downloader::finishedLoadingTags); + tagApi->load(); return; } - if (!m_oPagesC.isEmpty()) - { + if (!m_oPagesC.isEmpty()) { Page *page = m_oPagesC.takeFirst(); - if (m_lastPage != nullptr) - { page->setLastPage(m_lastPage); } + if (m_lastPage != nullptr) { + page->setLastPage(m_lastPage); + } m_lastPage = page; log("Loading count '" + page->url().toString() + "'"); page->loadTags(); return; } - if (!m_oPagesT.isEmpty()) - { + if (!m_oPagesT.isEmpty()) { Page *page = m_oPagesT.takeFirst(); - if (m_lastPage != nullptr) - { page->setLastPage(m_lastPage); } + if (m_lastPage != nullptr) { + page->setLastPage(m_lastPage); + } m_lastPage = page; log("Loading tags '" + page->url().toString() + "'"); page->loadTags(); return; } - if (!m_oPages.isEmpty()) - { + if (!m_oPages.isEmpty()) { Page *page = m_oPages.takeFirst(); - if (m_lastPage != nullptr) - { page->setLastPage(m_lastPage); } + if (m_lastPage != nullptr) { + page->setLastPage(m_lastPage); + } m_lastPage = page; log("Loading images '" + page->url().toString() + "'"); page->load(); return; } - if (!m_images.isEmpty()) - { + if (!m_images.isEmpty()) { const QSharedPointer image = m_images.takeFirst(); log(QString("Loading image '%1'").arg(image->url().toString())); - auto dwl = new ImageDownloader(m_profile, image, m_filename, m_location, 0, true, false, m_blacklist, this); + auto dwl = new ImageDownloader(m_profile, image, m_filename, m_location, 0, true, false, this); + if (!m_blacklist) { + dwl->setBlacklist(&m_blacklistedTags); + } connect(dwl, &ImageDownloader::saved, this, &Downloader::finishedLoadingImage); connect(dwl, &ImageDownloader::saved, dwl, &ImageDownloader::deleteLater); dwl->save(); return; } } -void Downloader::finishedLoadingTags(const QList &tags) +void Downloader::finishedLoadingTags(TagApi *api, TagApi::LoadResult status) { - if (m_cancelled) + if (m_cancelled) { + return; + } + + if (status == TagApi::LoadResult::Error) { + log(QStringLiteral("Error loading pure tags"), Logger::Warning); return; + } + const QList tags = api->tags(); log(QStringLiteral("Received pure tags (%1)").arg(tags.count())); + api->deleteLater(); m_results.append(tags); - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } QMutableListIterator i(m_results); - while (i.hasNext()) - if (i.next().count() < m_tagsMin) + while (i.hasNext()) { + if (i.next().count() < m_tagsMin) { i.remove(); + } + } - if (m_quit) + if (m_quit) { returnTagList(m_results); - else + } else { emit finishedTags(m_results); + } } void Downloader::getImages() { - if (m_sites.empty()) - { + if (m_sites.empty()) { std::cerr << "No valid source found" << std::endl; return; } @@ -273,13 +300,12 @@ void Downloader::getImages() m_waiting = 0; m_cancelled = false; - for (Site *site : qAsConst(m_sites)) - { + for (Site *site : qAsConst(m_sites)) { int pages = qCeil(static_cast(m_max) / m_perPage); - if (pages <= 0 || m_perPage <= 0 || m_max <= 0) + if (pages <= 0 || m_perPage <= 0 || m_max <= 0) { pages = 1; - for (int p = 0; p < pages; ++p) - { + } + for (int p = 0; p < pages; ++p) { Page *page = new Page(m_profile, site, m_sites, m_tags, m_page + p, m_perPage, m_postFiltering, true, this); connect(page, &Page::finishedLoading, this, &Downloader::finishedLoadingImages); @@ -289,62 +315,62 @@ void Downloader::getImages() } } - if (m_previous != nullptr) + if (m_previous != nullptr) { m_lastPage = m_previous->lastPage(); + } loadNext(); } void Downloader::finishedLoadingImages(Page *page) { - if (m_cancelled) + if (m_cancelled) { return; + } log(QStringLiteral("Received image page '%1' (%2)").arg(page->url().toString(), QString::number(page->images().count()))); emit finishedImagesPage(page); - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } QSet md5s; QList> images; - for (Page *p : qAsConst(m_pages)) - { - for (const QSharedPointer &img : p->images()) - { + for (Page *p : qAsConst(m_pages)) { + for (const QSharedPointer &img : p->images()) { // Blacklisted tags - if (!m_blacklist) - { - if (!m_blacklistedTags.match(img->tokens(m_profile)).empty()) - { + if (!m_blacklist) { + if (!m_blacklistedTags.match(img->tokens(m_profile)).empty()) { ++m_ignored; continue; } } // Skip duplicates - if (m_noDuplicates) - { - if (md5s.contains(img->md5())) + if (m_noDuplicates) { + if (md5s.contains(img->md5())) { continue; + } md5s.insert(img->md5()); } images.append(img); - if (images.count() == m_max) + if (images.count() == m_max) { break; + } } - if (images.count() == m_max) + if (images.count() == m_max) { break; + } } - if (m_quit) + if (m_quit) { downloadImages(images); - else + } else { emit finishedImages(images); + } } void Downloader::downloadImages(const QList> &images) @@ -355,32 +381,33 @@ void Downloader::downloadImages(const QList> &images) loadNext(); } -void Downloader::finishedLoadingImage(const QSharedPointer &image, const QMap &result) +void Downloader::finishedLoadingImage(const QSharedPointer &image, const QList &result) { Q_UNUSED(result); - if (m_cancelled) + if (m_cancelled) { return; + } log(QStringLiteral("Received image '%1'").arg(image->url().toString())); - if (!m_quit) + if (!m_quit) { emit finishedImage(image); + } - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } - if (m_quit) + if (m_quit) { returnString(QStringLiteral("Downloaded images successfully.")); + } } void Downloader::getUrls() { - if (m_sites.empty()) - { + if (m_sites.empty()) { std::cerr << "No valid source found" << std::endl; return; } @@ -390,13 +417,12 @@ void Downloader::getUrls() m_duplicates = 0; m_cancelled = false; - for (Site *site : qAsConst(m_sites)) - { + for (Site *site : qAsConst(m_sites)) { int pages = qCeil(static_cast(m_max) / m_perPage); - if (pages <= 0 || m_perPage <= 0 || m_max <= 0) + if (pages <= 0 || m_perPage <= 0 || m_max <= 0) { pages = 1; - for (int p = 0; p < pages; ++p) - { + } + for (int p = 0; p < pages; ++p) { Page *page = new Page(m_profile, site, m_sites, m_tags, m_page + p, m_perPage, m_postFiltering, true, this); connect(page, &Page::finishedLoading, this, &Downloader::finishedLoadingUrls); @@ -410,61 +436,62 @@ void Downloader::getUrls() } void Downloader::finishedLoadingUrls(Page *page) { - if (m_cancelled) + if (m_cancelled) { return; + } log(QStringLiteral("Received url page '%1' (%2)").arg(page->url().toString(), QString::number(page->images().count()))); emit finishedUrlsPage(page); - if (--m_waiting > 0) - { + if (--m_waiting > 0) { loadNext(); return; } QSet md5s; QVector> images; - for (Page *p : qAsConst(m_pages)) - { - for (const QSharedPointer &img : p->images()) - { + for (Page *p : qAsConst(m_pages)) { + for (const QSharedPointer &img : p->images()) { // Blacklisted tags - if (!m_blacklist) - { - if (!m_blacklistedTags.match(img->tokens(m_profile)).empty()) - { + if (!m_blacklist) { + if (!m_blacklistedTags.match(img->tokens(m_profile)).empty()) { ++m_ignored; continue; } } // Skip duplicates - if (m_noDuplicates) - { - if (md5s.contains(img->md5())) + if (m_noDuplicates) { + if (md5s.contains(img->md5())) { continue; + } md5s.insert(img->md5()); } images.append(img); - if (images.count() == m_max) + if (images.count() == m_max) { break; + } } - if (images.count() == m_max) + if (images.count() == m_max) { break; + } } QStringList urls; int i = 0; - for (const QSharedPointer &img : images) - if (m_max <= 0 || i++ < m_max) + for (const QSharedPointer &img : images) { + if (m_max <= 0 || i++ < m_max) { urls.append(img->url().toString()); + } + } - if (m_quit) + if (m_quit) { returnStringList(urls); - else + } else { emit finishedUrls(urls); + } } void Downloader::returnInt(int ret) @@ -479,8 +506,7 @@ void Downloader::returnString(const QString &ret) } void Downloader::returnTagList(const QList &tags) { - for (const Tag &tag : tags) - { + for (const Tag &tag : tags) { QString ret = m_tagsFormat; ret.replace("\\t", "\t"); ret.replace("\\n", "\n"); @@ -495,8 +521,9 @@ void Downloader::returnTagList(const QList &tags) } void Downloader::returnStringList(const QStringList &ret) { - for (const QString &str : ret) + for (const QString &str : ret) { std::cout << str.toStdString() << std::endl; + } emit quit(); } @@ -515,8 +542,9 @@ int Downloader::duplicatesCount() const int Downloader::pagesCount() const { int pages = qCeil(static_cast(m_max) / m_perPage); - if (pages <= 0 || m_perPage <= 0 || m_max <= 0) + if (pages <= 0 || m_perPage <= 0 || m_max <= 0) { pages = 1; + } return pages * m_sites.size(); } int Downloader::imagesMax() const diff --git a/lib/src/downloader/downloader.h b/lib/src/downloader/downloader.h index bdef8f0d7..84548ef11 100644 --- a/lib/src/downloader/downloader.h +++ b/lib/src/downloader/downloader.h @@ -12,8 +12,10 @@ #include "models/filtering/blacklist.h" #include "models/image.h" #include "tags/tag.h" +#include "tags/tag-api.h" +struct ImageSaveResult; class Profile; class Page; class Site; @@ -61,11 +63,11 @@ class Downloader : public QObject void returnTagList(const QList &tags); void returnStringList(const QStringList &ret); void finishedLoadingPageCount(Page *page); - void finishedLoadingTags(const QList &tags); + void finishedLoadingTags(TagApi *api, TagApi::LoadResult status); void finishedLoadingPageTags(Page *page); void finishedLoadingImages(Page *page); void finishedLoadingUrls(Page *page); - void finishedLoadingImage(const QSharedPointer &image, const QMap &result); + void finishedLoadingImage(const QSharedPointer &image, const QList &result); void cancel(); void clear(); diff --git a/lib/src/downloader/extension-rotator.cpp b/lib/src/downloader/extension-rotator.cpp index 6f9864576..efcb893a6 100644 --- a/lib/src/downloader/extension-rotator.cpp +++ b/lib/src/downloader/extension-rotator.cpp @@ -15,25 +15,28 @@ ExtensionRotator::ExtensionRotator(const QString &initialExtension, const QStrin const int index = extensions.indexOf(initialExtension); // If the initial extension is not in the list, we return the first one - if (index < 0) + if (index < 0) { m_next = 0; - else + } else { m_next = index + 1; + } } QString ExtensionRotator::next() { // Always return an empty string for empty lists - if (m_extensions.isEmpty()) + if (m_extensions.isEmpty()) { return QString(); + } QString next = m_extensions[m_next % m_extensions.length()]; // If we did a full loop, that means we finished const bool isLast = m_next == m_extensions.size(); const bool isNotFound = m_extensions.indexOf(m_initialExtension) < 0; - if (next == m_initialExtension || (isLast && isNotFound)) + if (next == m_initialExtension || (isLast && isNotFound)) { return QString(); + } m_next++; return next; diff --git a/lib/src/downloader/file-downloader.cpp b/lib/src/downloader/file-downloader.cpp index fb09b0589..bb1989da8 100644 --- a/lib/src/downloader/file-downloader.cpp +++ b/lib/src/downloader/file-downloader.cpp @@ -1,6 +1,7 @@ #include "downloader/file-downloader.h" #include #include "functions.h" +#include "logger.h" #define WRITE_BUFFER_SIZE (200 * 1024) @@ -22,8 +23,7 @@ bool FileDownloader::start(QNetworkReply *reply, const QStringList &paths) m_writeError = false; m_reply = reply; - if (ok) - { + if (ok) { connect(reply, &QNetworkReply::readyRead, this, &FileDownloader::replyReadyRead); connect(reply, &QNetworkReply::finished, this, &FileDownloader::replyFinished); } @@ -34,11 +34,11 @@ bool FileDownloader::start(QNetworkReply *reply, const QStringList &paths) void FileDownloader::replyReadyRead() { - if (m_reply->bytesAvailable() < WRITE_BUFFER_SIZE) + if (m_reply->bytesAvailable() < WRITE_BUFFER_SIZE) { return; + } - if (m_file.write(m_reply->readAll()) < 0) - { + if (m_file.write(m_reply->readAll()) < 0) { m_writeError = true; m_reply->abort(); } @@ -52,23 +52,22 @@ void FileDownloader::replyFinished() const bool failedLastWrite = data.length() > 0 && written < 0; const bool invalidHtml = !m_allowHtmlResponses && QString(data.left(100)).trimmed().startsWith("error() != QNetworkReply::NoError || failedLastWrite || invalidHtml) - { + if (m_reply->error() != QNetworkReply::NoError || failedLastWrite || invalidHtml) { m_file.remove(); - if (failedLastWrite || m_writeError) - { emit writeError(); } - else if (invalidHtml) - { + if (failedLastWrite || m_writeError) { + emit writeError(); + } else if (invalidHtml) { log(QString("Invalid HTML content returned for url '%1'").arg(m_reply->url().toString()), Logger::Info); emit networkError(QNetworkReply::ContentNotFoundError, "Invalid HTML content returned"); + } else { + emit networkError(m_reply->error(), m_reply->errorString()); } - else - { emit networkError(m_reply->error(), m_reply->errorString()); } return; } - for (const QString © : qAsConst(m_copies)) + for (const QString © : qAsConst(m_copies)) { m_file.copy(copy); + } emit success(); } diff --git a/lib/src/downloader/image-downloader.cpp b/lib/src/downloader/image-downloader.cpp index 74241ffa9..5befdd0dc 100644 --- a/lib/src/downloader/image-downloader.cpp +++ b/lib/src/downloader/image-downloader.cpp @@ -1,10 +1,13 @@ #include "downloader/image-downloader.h" +#include #include +#include #include #include "extension-rotator.h" #include "file-downloader.h" #include "functions.h" #include "logger.h" +#include "models/api/api.h" #include "models/filename.h" #include "models/image.h" #include "models/profile.h" @@ -25,18 +28,23 @@ static void addMd5(Profile *profile, const QString &path) } -ImageDownloader::ImageDownloader(Profile *profile, QSharedPointer img, QString filename, QString path, int count, bool addMd5, bool startCommands, bool getBlacklisted, QObject *parent, bool loadTags, bool rotate, bool force) - : QObject(parent), m_profile(profile), m_image(std::move(img)), m_fileDownloader(false, this), m_filename(std::move(filename)), m_path(std::move(path)), m_loadTags(loadTags), m_count(count), m_addMd5(addMd5), m_startCommands(startCommands), m_getBlacklisted(getBlacklisted), m_writeError(false), m_rotate(rotate), m_force(force) -{} +ImageDownloader::ImageDownloader(Profile *profile, QSharedPointer img, QString filename, QString path, int count, bool addMd5, bool startCommands, QObject *parent, bool loadTags, bool rotate, bool force, Image::Size size) + : QObject(parent), m_profile(profile), m_image(std::move(img)), m_fileDownloader(false, this), m_filename(std::move(filename)), m_path(std::move(path)), m_loadTags(loadTags), m_count(count), m_addMd5(addMd5), m_startCommands(startCommands), m_writeError(false), m_rotate(rotate), m_force(force) +{ + setSize(size); +} -ImageDownloader::ImageDownloader(Profile *profile, QSharedPointer img, QStringList paths, int count, bool addMd5, bool startCommands, bool getBlacklisted, QObject *parent, bool rotate, bool force) - : QObject(parent), m_profile(profile), m_image(std::move(img)), m_fileDownloader(false, this), m_loadTags(false), m_paths(std::move(paths)), m_count(count), m_addMd5(addMd5), m_startCommands(startCommands), m_getBlacklisted(getBlacklisted), m_writeError(false), m_rotate(rotate), m_force(force) -{} +ImageDownloader::ImageDownloader(Profile *profile, QSharedPointer img, QStringList paths, int count, bool addMd5, bool startCommands, QObject *parent, bool rotate, bool force, Image::Size size) + : QObject(parent), m_profile(profile), m_image(std::move(img)), m_fileDownloader(false, this), m_loadTags(false), m_paths(std::move(paths)), m_count(count), m_addMd5(addMd5), m_startCommands(startCommands), m_writeError(false), m_rotate(rotate), m_force(force) +{ + setSize(size); +} ImageDownloader::~ImageDownloader() { - if (m_reply != nullptr) + if (m_reply != nullptr) { m_reply->deleteLater(); + } } bool ImageDownloader::isRunning() const @@ -44,16 +52,39 @@ bool ImageDownloader::isRunning() const return m_reply != nullptr && m_reply->isRunning(); } +void ImageDownloader::setSize(Image::Size size) +{ + if (size == Image::Size::Unknown) { + const bool getOriginals = m_profile->getSettings()->value("Save/downloadoriginals", true).toBool(); + const bool hasSample = m_image->url(Image::Size::Sample).isEmpty(); + if (getOriginals || !hasSample) { + m_size = Image::Size::Full; + } else { + m_size = Image::Size::Sample; + } + } else { + m_size = size; + } +} + +void ImageDownloader::setBlacklist(Blacklist *blacklist) +{ + m_blacklist = blacklist; +} + void ImageDownloader::save() { + // Always load details if the API doesn't provide the file URL in the listing page + const QStringList forcedTokens = m_image->parentSite()->getApis().first()->forcedTokens(); + const bool needFileUrl = forcedTokens.contains("*") || forcedTokens.contains("file_url"); + // If we use direct saving or don't want to load tags, we directly save the image const int globalNeedTags = needExactTags(m_profile->getSettings()); - const int localNeedTags = Filename(m_filename).needExactTags(m_image->parentSite()); + const int localNeedTags = m_filename.needExactTags(m_image->parentSite()); const int needTags = qMax(globalNeedTags, localNeedTags); const bool filenameNeedTags = needTags == 2 || (needTags == 1 && m_image->hasUnknownTag()); - const bool blacklistNeedTags = m_getBlacklisted && m_image->tags().isEmpty(); - if (!blacklistNeedTags && (!m_loadTags || !m_paths.isEmpty() || !filenameNeedTags)) - { + const bool blacklistNeedTags = m_blacklist != nullptr && m_image->tags().isEmpty(); + if (!blacklistNeedTags && !needFileUrl && (!m_loadTags || !m_paths.isEmpty() || !filenameNeedTags)) { loadedSave(); return; } @@ -62,16 +93,16 @@ void ImageDownloader::save() m_image->loadDetails(); } -int ImageDownloader::needExactTags(QSettings *settings) +int ImageDownloader::needExactTags(QSettings *settings) const { int need = 0; const auto logFiles = getExternalLogFiles(settings); - for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) - { + for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) { need = qMax(need, Filename(it.value().value("content").toString()).needExactTags()); - if (need == 2) + if (need == 2) { return need; + } } QStringList settingNames = QStringList() @@ -83,15 +114,16 @@ int ImageDownloader::needExactTags(QSettings *settings) << "Exec/SQL/image" << "Exec/SQL/tag_after" << "Exec/SQL/after"; - for (const QString &setting : settingNames) - { + for (const QString &setting : settingNames) { const QString value = settings->value(setting, "").toString(); - if (value.isEmpty()) + if (value.isEmpty()) { continue; + } need = qMax(need, Filename(value).needExactTags()); - if (need == 2) + if (need == 2) { return need; + } } return need; @@ -99,79 +131,75 @@ int ImageDownloader::needExactTags(QSettings *settings) void ImageDownloader::abort() { - if (m_reply != nullptr && m_reply->isRunning()) + m_image->abortTags(); + if (m_reply != nullptr && m_reply->isRunning()) { m_reply->abort(); + } } void ImageDownloader::loadedSave() { // Get the download path from the image if possible - if (m_paths.isEmpty()) - { - m_paths = m_image->path(m_filename, m_path, m_count, true, true, true, true); + if (m_paths.isEmpty()) { + m_paths = m_image->paths(m_filename, m_path, m_count); // Use a random temporary file if we need the MD5 or equivalent - if (Filename(m_filename).needTemporaryFile(m_image->tokens(m_profile))) - { + if (m_filename.needTemporaryFile(m_image->tokens(m_profile))) { const QString tmpDir = !m_path.isEmpty() ? m_path : QDir::tempPath(); m_temporaryPath = tmpDir + "/" + QUuid::createUuid().toString().mid(1, 36) + ".tmp"; } } // Directly use the image path as temporary file if possible - if (m_temporaryPath.isEmpty()) - { m_temporaryPath = m_paths.first() + ".tmp"; } + if (m_temporaryPath.isEmpty()) { + m_temporaryPath = m_paths.first() + ".tmp"; + } // Check if the image is blacklisted - if (!m_getBlacklisted) - { - const QStringList &detected = m_profile->getBlacklist().match(m_image->tokens(m_profile)); - if (!detected.isEmpty()) - { + if (m_blacklist != nullptr) { + const QStringList &detected = m_blacklist->match(m_image->tokens(m_profile)); + if (!detected.isEmpty()) { log(QStringLiteral("Image contains blacklisted tags: '%1'").arg(detected.join("', '")), Logger::Info); - emit saved(m_image, makeMap(m_paths, Image::SaveResult::Blacklisted)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::Blacklisted)); return; } } // Try to save the image if it's already loaded or exists somewhere else on disk - if (!m_force) - { + if (!m_force) { // Check if the destination files already exist bool allExists = true; - for (const QString &path : qAsConst(m_paths)) - { - if (!QFile::exists(path)) - { + for (const QString &path : qAsConst(m_paths)) { + if (!QFile::exists(path)) { allExists = false; break; } } - if (allExists) - { + if (allExists) { log(QStringLiteral("File already exists: `%1`").arg(m_paths.first()), Logger::Info); - for (const QString &path : qAsConst(m_paths)) - { addMd5(m_profile, path); } - emit saved(m_image, makeMap(m_paths, Image::SaveResult::AlreadyExistsDisk)); + for (const QString &path : qAsConst(m_paths)) { + addMd5(m_profile, path); + } + emit saved(m_image, makeResult(m_paths, Image::SaveResult::AlreadyExistsDisk)); return; } // If we don't need any loading, we can return already - Image::SaveResult res = m_image->preSave(m_temporaryPath); - if (res != Image::SaveResult::NotLoaded) - { - QMap result {{ m_temporaryPath, res }}; + Image::SaveResult res = m_image->preSave(m_temporaryPath, m_size); + if (res != Image::SaveResult::NotLoaded) { + QList result {{ m_temporaryPath, m_size, res }}; - if (res == Image::SaveResult::Saved || res == Image::SaveResult::Copied || res == Image::SaveResult::Moved || res == Image::SaveResult::Linked) - { result = postSaving(res); } + if (res == Image::SaveResult::Saved || res == Image::SaveResult::Copied || res == Image::SaveResult::Moved || res == Image::SaveResult::Linked) { + result = postSaving(res); + } emit saved(m_image, result); return; } } - m_url = m_image->url(Image::Size::Full); - log(QStringLiteral("Loading and saving image in `%1`").arg(m_paths.first())); + m_url = m_image->url(m_size); + log(QStringLiteral("Loading and saving image from `%1` in `%2`").arg(m_url.toString(), m_paths.first())); loadImage(); } @@ -182,8 +210,9 @@ void ImageDownloader::loadImage() connect(&m_fileDownloader, &FileDownloader::writeError, this, &ImageDownloader::writeError, Qt::UniqueConnection); // Delete previous replies for retries - if (m_reply != nullptr) - { m_reply->deleteLater(); } + if (m_reply != nullptr) { + m_reply->deleteLater(); + } // Load the image directly on the disk Site *site = m_image->parentSite(); @@ -193,83 +222,79 @@ void ImageDownloader::loadImage() // Create download root directory const QString rootDir = m_temporaryPath.section(QDir::separator(), 0, -2); - if (!QDir(rootDir).exists() && !QDir().mkpath(rootDir)) - { + if (!QDir(rootDir).exists() && !QDir().mkpath(rootDir)) { log(QStringLiteral("Impossible to create the destination folder: %1.").arg(rootDir), Logger::Error); - emit saved(m_image, makeMap(m_paths, Image::SaveResult::Error)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::Error)); return; } // If we can't start writing for some reason, return an error - if (!m_fileDownloader.start(m_reply, QStringList() << m_temporaryPath)) - { + if (!m_fileDownloader.start(m_reply, QStringList() << m_temporaryPath)) { log(QStringLiteral("Unable to open file"), Logger::Error); - emit saved(m_image, makeMap(m_paths, Image::SaveResult::Error)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::Error)); return; } } void ImageDownloader::downloadProgressImage(qint64 v1, qint64 v2) { - if (m_image->fileSize() == 0 || m_image->fileSize() < v2 / 2) - m_image->setFileSize(v2); + if (m_image->fileSize() == 0 || m_image->fileSize() < v2 / 2) { + m_image->setFileSize(v2, currentSize()); + } emit downloadProgress(m_image, v1, v2); } -QMap ImageDownloader::makeMap(const QStringList &keys, Image::SaveResult value) +Image::Size ImageDownloader::currentSize() const +{ + return m_tryingSample ? Image::Size::Sample : m_size; +} + +QList ImageDownloader::makeResult(const QStringList &paths, Image::SaveResult result) const { - QMap res; - for (const QString &key : keys) - res.insert(key, value); + const Image::Size size = currentSize(); + + QList res; + for (const QString &path : paths) { + res.append({ path, size, result }); + } return res; } void ImageDownloader::writeError() { - emit saved(m_image, makeMap(m_paths, Image::SaveResult::Error)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::Error)); } void ImageDownloader::networkError(QNetworkReply::NetworkError error, const QString &msg) { - if (error == QNetworkReply::ContentNotFoundError) - { + if (error == QNetworkReply::ContentNotFoundError) { QSettings *settings = m_profile->getSettings(); ExtensionRotator *extensionRotator = m_image->extensionRotator(); const bool sampleFallback = settings->value("Save/samplefallback", true).toBool(); + const bool shouldFallback = m_size == Image::Size::Full && sampleFallback && !m_image->url(Image::Size::Sample).isEmpty(); QString newext = extensionRotator != nullptr ? extensionRotator->next() : QString(); - const bool shouldFallback = sampleFallback && !m_image->url(Image::Size::Sample).isEmpty(); - const bool isLast = newext.isEmpty() || (shouldFallback && m_tryingSample); - - if (m_rotate && (!isLast || (shouldFallback && !m_tryingSample))) - { - if (isLast) - { - m_url = m_image->url(Image::Size::Sample); - m_tryingSample = true; - log(QStringLiteral("Image not found. New try with its sample...")); - } - else - { - m_url = setExtension(m_image->url(Image::Size::Full), newext); - log(QStringLiteral("Image not found. New try with extension %1 (%2)...").arg(newext, m_url.toString())); - } - + if (m_rotate && !newext.isEmpty()) { + m_url = setExtension(m_image->url(m_size), newext); + log(QStringLiteral("Image not found. New try with extension %1 (%2)...").arg(newext, m_url.toString())); m_image->setUrl(m_url); loadImage(); - } - else - { + } else if (shouldFallback && !m_tryingSample) { + m_url = m_image->url(Image::Size::Sample); + m_tryingSample = true; + log(QStringLiteral("Image not found. New try with its sample (%1)...").arg(m_url.toString())); + m_image->setUrl(m_url); + loadImage(); + } else { + m_tryingSample = false; log(QStringLiteral("Image not found.")); - emit saved(m_image, makeMap(m_paths, Image::SaveResult::NotFound)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::NotFound)); } - } - else if (error != QNetworkReply::OperationCanceledError) - { + } else if (error != QNetworkReply::OperationCanceledError) { log(QStringLiteral("Network error for the image: `%1`: %2 (%3)").arg(m_image->url().toString().toHtmlEscaped()).arg(error).arg(msg), Logger::Error); - emit saved(m_image, makeMap(m_paths, Image::SaveResult::NetworkError)); + emit saved(m_image, makeResult(m_paths, Image::SaveResult::NetworkError)); } } @@ -277,8 +302,7 @@ void ImageDownloader::success() { // Handle network redirects QUrl redir = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redir.isEmpty()) - { + if (!redir.isEmpty()) { m_url = redir; loadImage(); return; @@ -287,71 +311,76 @@ void ImageDownloader::success() emit saved(m_image, postSaving()); } -QMap ImageDownloader::postSaving(Image::SaveResult saveResult) +QList ImageDownloader::postSaving(Image::SaveResult saveResult) { const QString multipleFiles = m_profile->getSettings()->value("Save/multiple_files", "copy").toString(); + const Image::Size size = currentSize(); + + m_image->setSavePath(m_temporaryPath, size); - m_image->setSavePath(m_temporaryPath); + if (m_image->size(size).isEmpty()) { + QImageReader reader(m_temporaryPath); + QSize imgSize = reader.size(); + if (imgSize.isValid()) { + m_image->setSize(imgSize, size); + } + } - if (!m_filename.isEmpty()) - { m_paths = m_image->path(m_filename, m_path, m_count, true, true, true, true); } + if (!m_filename.format().isEmpty()) { + m_paths = m_image->paths(m_filename, m_path, m_count); + } QString suffix; #ifdef Q_OS_WIN - if (saveResult == Image::SaveResult::Linked) - { suffix = ".lnk"; } + if (saveResult == Image::SaveResult::Linked) { + suffix = ".lnk"; + } #endif QFile tmp(m_temporaryPath + suffix); bool moved = false; - QMap result; - for (const QString &file : qAsConst(m_paths)) - { + QList result; + for (const QString &file : qAsConst(m_paths)) { const QString path = file + suffix; // Don't overwrite already existing files - if (QFile::exists(file) || (!suffix.isEmpty() && QFile::exists(path))) - { + if (QFile::exists(file) || (!suffix.isEmpty() && QFile::exists(path))) { log(QStringLiteral("File already exists: `%1`").arg(file), Logger::Info); - if (suffix.isEmpty()) - { addMd5(m_profile, file); } - result[path] = Image::SaveResult::AlreadyExistsDisk; + if (suffix.isEmpty()) { + addMd5(m_profile, file); + } + result.append({ path, size, Image::SaveResult::AlreadyExistsDisk }); continue; } - if (!moved) - { + if (!moved) { const QString dir = path.section(QDir::separator(), 0, -2); - if (!QDir(dir).exists() && !QDir().mkpath(dir)) - { + if (!QDir(dir).exists() && !QDir().mkpath(dir)) { log(QStringLiteral("Impossible to create the destination folder: %1.").arg(dir), Logger::Error); - result[path] = Image::SaveResult::Error; + result.append({ path, size, Image::SaveResult::Error }); continue; } tmp.rename(path); moved = true; - } - else if (multipleFiles == "link") - { + } else if (multipleFiles == "link") { #ifdef Q_OS_WIN tmp.link(path + ".lnk"); #else tmp.link(path); #endif + } else { + tmp.copy(path); } - else - { tmp.copy(path); } - if (!result.contains(path)) - { result[path] = saveResult; } - - m_image->postSave(path, result[path], m_addMd5, m_startCommands, m_count); + result.append({ path, size, saveResult }); + m_image->postSave(path, size, saveResult, m_addMd5, m_startCommands, m_count); } - if (!moved) - { tmp.remove(); } + if (!moved) { + tmp.remove(); + } return result; } diff --git a/lib/src/downloader/image-downloader.h b/lib/src/downloader/image-downloader.h index 79133e636..fc4187702 100644 --- a/lib/src/downloader/image-downloader.h +++ b/lib/src/downloader/image-downloader.h @@ -1,7 +1,7 @@ #ifndef IMAGE_DOWNLOADER_H #define IMAGE_DOWNLOADER_H -#include +#include #include #include #include @@ -9,10 +9,13 @@ #include #include #include "downloader/file-downloader.h" +#include "downloader/image-save-result.h" #include "loader/downloadable.h" +#include "models/filename.h" #include "models/image.h" +class Blacklist; class Profile; class ImageDownloader : public QObject @@ -20,23 +23,26 @@ class ImageDownloader : public QObject Q_OBJECT public: - ImageDownloader(Profile *profile, QSharedPointer img, QString filename, QString path, int count, bool addMd5, bool startCommands, bool getBlacklisted, QObject *parent = nullptr, bool loadTags = true, bool rotate = true, bool force = false); - ImageDownloader(Profile *profile, QSharedPointer img, QStringList paths, int count, bool addMd5, bool startCommands, bool getBlacklisted, QObject *parent = nullptr, bool rotate = true, bool force = false); + ImageDownloader(Profile *profile, QSharedPointer img, QString filename, QString path, int count, bool addMd5, bool startCommands, QObject *parent = nullptr, bool loadTags = true, bool rotate = true, bool force = false, Image::Size size = Image::Size::Unknown); + ImageDownloader(Profile *profile, QSharedPointer img, QStringList paths, int count, bool addMd5, bool startCommands, QObject *parent = nullptr, bool rotate = true, bool force = false, Image::Size size = Image::Size::Unknown); ~ImageDownloader(); bool isRunning() const; + void setSize(Image::Size size); + void setBlacklist(Blacklist *blacklist); public slots: void save(); void abort(); protected: - int needExactTags(QSettings *settings); - QMap makeMap(const QStringList &keys, Image::SaveResult value); - QMap postSaving(Image::SaveResult saveResult = Image::SaveResult::Saved); + int needExactTags(QSettings *settings) const; + Image::Size currentSize() const; + QList makeResult(const QStringList &paths, Image::SaveResult result) const; + QList postSaving(Image::SaveResult saveResult = Image::SaveResult::Saved); signals: void downloadProgress(QSharedPointer img, qint64 v1, qint64 v2); - void saved(QSharedPointer img, const QMap &result); + void saved(QSharedPointer img, const QList &result); private slots: void loadedSave(); @@ -48,9 +54,10 @@ class ImageDownloader : public QObject private: Profile *m_profile; + Blacklist *m_blacklist = nullptr; QSharedPointer m_image; FileDownloader m_fileDownloader; - QString m_filename; + Filename m_filename; QString m_path; bool m_loadTags; QStringList m_paths; @@ -58,10 +65,10 @@ class ImageDownloader : public QObject int m_count; bool m_addMd5; bool m_startCommands; - bool m_getBlacklisted; bool m_writeError; bool m_rotate; bool m_force; + Image::Size m_size = Image::Size::Unknown; QNetworkReply *m_reply = nullptr; QUrl m_url; diff --git a/lib/src/downloader/image-save-result.cpp b/lib/src/downloader/image-save-result.cpp new file mode 100644 index 000000000..db315cba3 --- /dev/null +++ b/lib/src/downloader/image-save-result.cpp @@ -0,0 +1,14 @@ +#include "image-save-result.h" + + +bool operator==(const ImageSaveResult &lhs, const ImageSaveResult &rhs) +{ + return lhs.path == rhs.path + && lhs.size == rhs.size + && lhs.result == rhs.result; +} + +bool operator!=(const ImageSaveResult &lhs, const ImageSaveResult &rhs) +{ + return !(lhs == rhs); +} diff --git a/lib/src/downloader/image-save-result.h b/lib/src/downloader/image-save-result.h new file mode 100644 index 000000000..fed44efd7 --- /dev/null +++ b/lib/src/downloader/image-save-result.h @@ -0,0 +1,21 @@ +#ifndef IMAGE_SAVE_RESULT_H +#define IMAGE_SAVE_RESULT_H + +#include +#include +#include "models/image.h" + + +struct ImageSaveResult +{ + QString path; + Image::Size size; + Image::SaveResult result; +}; + +bool operator==(const ImageSaveResult &lhs, const ImageSaveResult &rhs); +bool operator!=(const ImageSaveResult &lhs, const ImageSaveResult &rhs); + +Q_DECLARE_METATYPE(ImageSaveResult) + +#endif // IMAGE_SAVE_RESULT_H diff --git a/lib/src/exponential-moving-average.cpp b/lib/src/exponential-moving-average.cpp new file mode 100644 index 000000000..101c1981f --- /dev/null +++ b/lib/src/exponential-moving-average.cpp @@ -0,0 +1,34 @@ +#include "exponential-moving-average.h" + + +ExponentialMovingAverage::ExponentialMovingAverage(double smoothingFactor) + : m_smoothingFactor(smoothingFactor) +{} + + +double ExponentialMovingAverage::average() const +{ + return m_average; +} + +void ExponentialMovingAverage::setSmoothingFactor(double smoothingFactor) +{ + m_smoothingFactor = smoothingFactor; +} + +void ExponentialMovingAverage::addValue(double value) +{ + if (!m_hasValue) { + m_average = value; + m_hasValue = true; + return; + } + + m_average = m_smoothingFactor * value + (1 - m_smoothingFactor) * m_average; +} + +void ExponentialMovingAverage::clear() +{ + m_average = 0; + m_hasValue = false; +} diff --git a/lib/src/exponential-moving-average.h b/lib/src/exponential-moving-average.h new file mode 100644 index 000000000..41166e8fc --- /dev/null +++ b/lib/src/exponential-moving-average.h @@ -0,0 +1,21 @@ +#ifndef EXPONENTIAL_MOVING_AVERAGE_H +#define EXPONENTIAL_MOVING_AVERAGE_H + + +class ExponentialMovingAverage +{ + public: + ExponentialMovingAverage() = default; + explicit ExponentialMovingAverage(double smoothingFactor); + double average() const; + void setSmoothingFactor(double smoothingFactor); + void addValue(double value); + void clear(); + + private: + double m_smoothingFactor; + double m_average = 0; + bool m_hasValue = false; +}; + +#endif // EXPONENTIAL_MOVING_AVERAGE_H diff --git a/lib/src/filename/ast-filename.cpp b/lib/src/filename/ast-filename.cpp new file mode 100644 index 000000000..36c8eec61 --- /dev/null +++ b/lib/src/filename/ast-filename.cpp @@ -0,0 +1,49 @@ +#include "filename/ast-filename.h" +#include "filename/ast/filename-node-root.h" +#include "filename/filename-parser.h" +#include "filename/filename-resolution-visitor.h" + + +AstFilename::AstFilename(const QString &str) + : m_parser(str) +{} + +void AstFilename::parse() +{ + auto ast = m_parser.parseRoot(); + if (m_parser.error().isEmpty()) { + m_ast = ast; + + FilenameResolutionVisitor resolutionVisitor; + m_tokens = resolutionVisitor.run(*m_ast); + } + + m_parsed = true; +} + +const QString &AstFilename::error() +{ + if (!m_parsed) { + parse(); + } + + return m_parser.error(); +} + +FilenameNodeRoot *AstFilename::ast() +{ + if (!m_parsed) { + parse(); + } + + return m_ast; +} + +const QSet &AstFilename::tokens() +{ + if (!m_parsed) { + parse(); + } + + return m_tokens; +} diff --git a/lib/src/filename/ast-filename.h b/lib/src/filename/ast-filename.h new file mode 100644 index 000000000..58046df71 --- /dev/null +++ b/lib/src/filename/ast-filename.h @@ -0,0 +1,30 @@ +#ifndef AST_FILENAME_H +#define AST_FILENAME_H + +#include +#include +#include "filename/filename-parser.h" + + +struct FilenameNodeRoot; + +class AstFilename +{ + public: + explicit AstFilename(const QString &str); + const QString &error(); + FilenameNodeRoot *ast(); + const QSet &tokens(); + + protected: + void parse(); + + private: + FilenameParser m_parser; + bool m_parsed = false; + + FilenameNodeRoot *m_ast = nullptr; + QSet m_tokens; +}; + +#endif // AST_FILENAME_H diff --git a/lib/src/filename/ast/filename-node-condition-ignore.cpp b/lib/src/filename/ast/filename-node-condition-ignore.cpp new file mode 100644 index 000000000..b872ed911 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-ignore.cpp @@ -0,0 +1,17 @@ +#include "filename/ast/filename-node-condition-ignore.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditionIgnore::FilenameNodeConditionIgnore(FilenameNodeCondition *node) + : node(node) +{} + +FilenameNodeConditionIgnore::~FilenameNodeConditionIgnore() +{ + delete node; +} + +void FilenameNodeConditionIgnore::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-ignore.h b/lib/src/filename/ast/filename-node-condition-ignore.h new file mode 100644 index 000000000..26e00c0c0 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-ignore.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_CONDITION_IGNORE_H +#define FILENAME_NODE_CONDITION_IGNORE_H + +#include "filename/ast/filename-node-condition.h" + + +struct FilenameNodeConditionIgnore : public FilenameNodeCondition +{ + FilenameNodeCondition *node; + + explicit FilenameNodeConditionIgnore(FilenameNodeCondition *node); + ~FilenameNodeConditionIgnore() override; + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_IGNORE_H diff --git a/lib/src/filename/ast/filename-node-condition-invert.cpp b/lib/src/filename/ast/filename-node-condition-invert.cpp new file mode 100644 index 000000000..d56621534 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-invert.cpp @@ -0,0 +1,17 @@ +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditionInvert::FilenameNodeConditionInvert(FilenameNodeCondition *node) + : node(node) +{} + +FilenameNodeConditionInvert::~FilenameNodeConditionInvert() +{ + delete node; +} + +void FilenameNodeConditionInvert::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-invert.h b/lib/src/filename/ast/filename-node-condition-invert.h new file mode 100644 index 000000000..6708d2083 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-invert.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_CONDITION_INVERT_H +#define FILENAME_NODE_CONDITION_INVERT_H + +#include "filename/ast/filename-node-condition.h" + + +struct FilenameNodeConditionInvert : public FilenameNodeCondition +{ + FilenameNodeCondition *node; + + explicit FilenameNodeConditionInvert(FilenameNodeCondition *node); + ~FilenameNodeConditionInvert() override; + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_INVERT_H diff --git a/lib/src/filename/ast/filename-node-condition-javascript.cpp b/lib/src/filename/ast/filename-node-condition-javascript.cpp new file mode 100644 index 000000000..d33a70cac --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-javascript.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-condition-javascript.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditionJavaScript::FilenameNodeConditionJavaScript(QString script) + : script(std::move(script)) +{} + +void FilenameNodeConditionJavaScript::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-javascript.h b/lib/src/filename/ast/filename-node-condition-javascript.h new file mode 100644 index 000000000..dfd6ecc74 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-javascript.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_CONDITION_JAVASCRIPT_H +#define FILENAME_NODE_CONDITION_JAVASCRIPT_H + +#include +#include "filename/ast/filename-node-condition.h" + + +struct FilenameNodeConditionJavaScript : public FilenameNodeCondition +{ + QString script; + + explicit FilenameNodeConditionJavaScript(QString script); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_JAVASCRIPT_H diff --git a/lib/src/filename/ast/filename-node-condition-op.cpp b/lib/src/filename/ast/filename-node-condition-op.cpp new file mode 100644 index 000000000..afe1388f9 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-op.cpp @@ -0,0 +1,18 @@ +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditionOp::FilenameNodeConditionOp(Operator op, FilenameNodeCondition *left, FilenameNodeCondition *right) + : op(op), left(left), right(right) +{} + +FilenameNodeConditionOp::~FilenameNodeConditionOp() +{ + delete left; + delete right; +} + +void FilenameNodeConditionOp::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-op.h b/lib/src/filename/ast/filename-node-condition-op.h new file mode 100644 index 000000000..28e4f074d --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-op.h @@ -0,0 +1,24 @@ +#ifndef FILENAME_NODE_CONDITION_OP_H +#define FILENAME_NODE_CONDITION_OP_H + +#include "filename/ast/filename-node-condition.h" + + +struct FilenameNodeConditionOp : public FilenameNodeCondition +{ + enum Operator + { + And, + Or, + }; + + Operator op; + FilenameNodeCondition *left; + FilenameNodeCondition *right; + + FilenameNodeConditionOp(Operator op, FilenameNodeCondition *left, FilenameNodeCondition *right); + ~FilenameNodeConditionOp() override; + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_OP_H diff --git a/lib/src/filename/ast/filename-node-condition-tag.cpp b/lib/src/filename/ast/filename-node-condition-tag.cpp new file mode 100644 index 000000000..1765c1e03 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-tag.cpp @@ -0,0 +1,21 @@ +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-visitor.h" +#include "models/filtering/filter.h" +#include "models/filtering/filter-factory.h" + + +FilenameNodeConditionTag::FilenameNodeConditionTag(Tag tag) + : tag(std::move(tag)) +{ + filter = FilterFactory::build(this->tag.text()); +} + +FilenameNodeConditionTag::~FilenameNodeConditionTag() +{ + delete filter; +} + +void FilenameNodeConditionTag::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-tag.h b/lib/src/filename/ast/filename-node-condition-tag.h new file mode 100644 index 000000000..8aeb46a7b --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-tag.h @@ -0,0 +1,20 @@ +#ifndef FILENAME_NODE_CONDITION_TAG_H +#define FILENAME_NODE_CONDITION_TAG_H + +#include "filename/ast/filename-node-condition.h" +#include "tags/tag.h" + + +class Filter; + +struct FilenameNodeConditionTag : public FilenameNodeCondition +{ + Tag tag; + Filter *filter; + + explicit FilenameNodeConditionTag(Tag tag); + ~FilenameNodeConditionTag() override; + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_TAG_H diff --git a/lib/src/filename/ast/filename-node-condition-token.cpp b/lib/src/filename/ast/filename-node-condition-token.cpp new file mode 100644 index 000000000..4f318ff52 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-token.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditionToken::FilenameNodeConditionToken(QString token) + : token(std::move(token)) +{} + +void FilenameNodeConditionToken::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-condition-token.h b/lib/src/filename/ast/filename-node-condition-token.h new file mode 100644 index 000000000..e05346126 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition-token.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_CONDITION_TOKEN_H +#define FILENAME_NODE_CONDITION_TOKEN_H + +#include +#include "filename/ast/filename-node-condition.h" + + +struct FilenameNodeConditionToken : public FilenameNodeCondition +{ + QString token; + + explicit FilenameNodeConditionToken(QString token); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITION_TOKEN_H diff --git a/lib/src/filename/ast/filename-node-condition.h b/lib/src/filename/ast/filename-node-condition.h new file mode 100644 index 000000000..f181f4c34 --- /dev/null +++ b/lib/src/filename/ast/filename-node-condition.h @@ -0,0 +1,11 @@ +#ifndef FILENAME_NODE_CONDITION_H +#define FILENAME_NODE_CONDITION_H + +#include "filename/ast/filename-node.h" + + +struct FilenameNodeCondition : public FilenameNode +{ +}; + +#endif // FILENAME_NODE_CONDITION_H diff --git a/lib/src/filename/ast/filename-node-conditional.cpp b/lib/src/filename/ast/filename-node-conditional.cpp new file mode 100644 index 000000000..ecc5b3230 --- /dev/null +++ b/lib/src/filename/ast/filename-node-conditional.cpp @@ -0,0 +1,20 @@ +#include "filename/ast/filename-node-conditional.h" +#include "filename/ast/filename-node-condition.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeConditional::FilenameNodeConditional(FilenameNodeCondition *condition, FilenameNode *ifTrue, FilenameNode *ifFalse) + : condition(condition), ifTrue(ifTrue), ifFalse(ifFalse) +{} + +FilenameNodeConditional::~FilenameNodeConditional() +{ + delete condition; + delete ifTrue; + delete ifFalse; +} + +void FilenameNodeConditional::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-conditional.h b/lib/src/filename/ast/filename-node-conditional.h new file mode 100644 index 000000000..153f7e1eb --- /dev/null +++ b/lib/src/filename/ast/filename-node-conditional.h @@ -0,0 +1,20 @@ +#ifndef FILENAME_NODE_CONDITIONAL_H +#define FILENAME_NODE_CONDITIONAL_H + +#include "filename/ast/filename-node.h" + + +struct FilenameNodeCondition; + +struct FilenameNodeConditional : public FilenameNode +{ + FilenameNodeCondition *condition; + FilenameNode *ifTrue; + FilenameNode *ifFalse; + + FilenameNodeConditional(FilenameNodeCondition *condition, FilenameNode *ifTrue, FilenameNode *ifFalse); + ~FilenameNodeConditional() override; + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_CONDITIONAL_H diff --git a/lib/src/filename/ast/filename-node-javascript.cpp b/lib/src/filename/ast/filename-node-javascript.cpp new file mode 100644 index 000000000..9d3c32e0f --- /dev/null +++ b/lib/src/filename/ast/filename-node-javascript.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-javascript.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeJavaScript::FilenameNodeJavaScript(QString script) + : script(std::move(script)) +{} + +void FilenameNodeJavaScript::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-javascript.h b/lib/src/filename/ast/filename-node-javascript.h new file mode 100644 index 000000000..5eb8a37f5 --- /dev/null +++ b/lib/src/filename/ast/filename-node-javascript.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_JAVASCRIPT_H +#define FILENAME_NODE_JAVASCRIPT_H + +#include +#include "filename/ast/filename-node.h" + + +struct FilenameNodeJavaScript : public FilenameNode +{ + QString script; + + explicit FilenameNodeJavaScript(QString script); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_JAVASCRIPT_H diff --git a/lib/src/filename/ast/filename-node-root.cpp b/lib/src/filename/ast/filename-node-root.cpp new file mode 100644 index 000000000..43399f553 --- /dev/null +++ b/lib/src/filename/ast/filename-node-root.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeRoot::FilenameNodeRoot(QList exprs) + : exprs(std::move(exprs)) +{} + +void FilenameNodeRoot::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-root.h b/lib/src/filename/ast/filename-node-root.h new file mode 100644 index 000000000..fd83a0696 --- /dev/null +++ b/lib/src/filename/ast/filename-node-root.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_ROOT_H +#define FILENAME_NODE_ROOT_H + +#include +#include "filename/ast/filename-node.h" + + +struct FilenameNodeRoot : public FilenameNode +{ + QList exprs; + + explicit FilenameNodeRoot(QList exprs); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_ROOT_H diff --git a/lib/src/filename/ast/filename-node-text.cpp b/lib/src/filename/ast/filename-node-text.cpp new file mode 100644 index 000000000..83a53c055 --- /dev/null +++ b/lib/src/filename/ast/filename-node-text.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-text.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeText::FilenameNodeText(QString text) + : text(std::move(text)) +{} + +void FilenameNodeText::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-text.h b/lib/src/filename/ast/filename-node-text.h new file mode 100644 index 000000000..c98833fc7 --- /dev/null +++ b/lib/src/filename/ast/filename-node-text.h @@ -0,0 +1,16 @@ +#ifndef FILENAME_NODE_TEXT_H +#define FILENAME_NODE_TEXT_H + +#include +#include "filename/ast/filename-node.h" + + +struct FilenameNodeText : public FilenameNode +{ + QString text; + + explicit FilenameNodeText(QString text); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_TEXT_H diff --git a/lib/src/filename/ast/filename-node-variable.cpp b/lib/src/filename/ast/filename-node-variable.cpp new file mode 100644 index 000000000..21de73f5a --- /dev/null +++ b/lib/src/filename/ast/filename-node-variable.cpp @@ -0,0 +1,12 @@ +#include "filename/ast/filename-node-variable.h" +#include "filename/ast/filename-visitor.h" + + +FilenameNodeVariable::FilenameNodeVariable(QString name, QMap opts) + : name(std::move(name)), opts(std::move(opts)) +{} + +void FilenameNodeVariable::accept(FilenameVisitor &v) const +{ + v.visit(*this); +} diff --git a/lib/src/filename/ast/filename-node-variable.h b/lib/src/filename/ast/filename-node-variable.h new file mode 100644 index 000000000..19d1eb1f0 --- /dev/null +++ b/lib/src/filename/ast/filename-node-variable.h @@ -0,0 +1,18 @@ +#ifndef FILENAME_NODE_VARIABLE_H +#define FILENAME_NODE_VARIABLE_H + +#include +#include +#include "filename/ast/filename-node.h" + + +struct FilenameNodeVariable : public FilenameNode +{ + QString name; + QMap opts; + + explicit FilenameNodeVariable(QString name, QMap opts); + void accept(FilenameVisitor &v) const override; +}; + +#endif // FILENAME_NODE_VARIABLE_H diff --git a/lib/src/filename/ast/filename-node.h b/lib/src/filename/ast/filename-node.h new file mode 100644 index 000000000..c5e831c76 --- /dev/null +++ b/lib/src/filename/ast/filename-node.h @@ -0,0 +1,13 @@ +#ifndef FILENAME_NODE_H +#define FILENAME_NODE_H + +class FilenameVisitor; + + +struct FilenameNode +{ + virtual ~FilenameNode() = default; + virtual void accept(FilenameVisitor &v) const = 0; +}; + +#endif // FILENAME_NODE_H diff --git a/lib/src/filename/ast/filename-visitor-base.cpp b/lib/src/filename/ast/filename-visitor-base.cpp new file mode 100644 index 000000000..648dc70ba --- /dev/null +++ b/lib/src/filename/ast/filename-visitor-base.cpp @@ -0,0 +1,81 @@ +#include "filename/ast/filename-visitor-base.h" +#include +#include +#include +#include "filename/ast/filename-node-condition.h" +#include "filename/ast/filename-node-condition-ignore.h" +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-node-condition-javascript.h" +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-conditional.h" +#include "filename/ast/filename-node-javascript.h" +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-node-text.h" +#include "filename/ast/filename-node-variable.h" +#include "loader/token.h" + + +void FilenameVisitorBase::visit(const FilenameNodeConditional &node) +{ + node.condition->accept(*this); + node.ifTrue->accept(*this); + + if (node.ifFalse != nullptr) { + node.ifFalse->accept(*this); + } +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionIgnore &node) +{ + node.node->accept(*this); +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionInvert &node) +{ + node.node->accept(*this); +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionJavaScript &node) +{ + Q_UNUSED(node); // No-op +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionOp &node) +{ + node.left->accept(*this); + node.right->accept(*this); +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionTag &node) +{ + Q_UNUSED(node); // No-op +} + +void FilenameVisitorBase::visit(const FilenameNodeConditionToken &node) +{ + Q_UNUSED(node); // No-op +} + +void FilenameVisitorBase::visit(const FilenameNodeJavaScript &node) +{ + Q_UNUSED(node); // No-op +} + +void FilenameVisitorBase::visit(const FilenameNodeRoot &node) +{ + for (auto expr : node.exprs) { + expr->accept(*this); + } +} + +void FilenameVisitorBase::visit(const FilenameNodeText &node) +{ + Q_UNUSED(node); // No-op +} + +void FilenameVisitorBase::visit(const FilenameNodeVariable &node) +{ + Q_UNUSED(node); // No-op +} diff --git a/lib/src/filename/ast/filename-visitor-base.h b/lib/src/filename/ast/filename-visitor-base.h new file mode 100644 index 000000000..e175f7c7d --- /dev/null +++ b/lib/src/filename/ast/filename-visitor-base.h @@ -0,0 +1,23 @@ +#ifndef FILENAME_VISITOR_BASE_H +#define FILENAME_VISITOR_BASE_H + +#include "filename/ast/filename-visitor.h" + + +class FilenameVisitorBase : public FilenameVisitor +{ + public: + void visit(const FilenameNodeConditional &node) override; + void visit(const FilenameNodeConditionIgnore &node) override; + void visit(const FilenameNodeConditionInvert &node) override; + void visit(const FilenameNodeConditionJavaScript &node) override; + void visit(const FilenameNodeConditionOp &node) override; + void visit(const FilenameNodeConditionTag &node) override; + void visit(const FilenameNodeConditionToken &node) override; + void visit(const FilenameNodeJavaScript &node) override; + void visit(const FilenameNodeRoot &node) override; + void visit(const FilenameNodeText &node) override; + void visit(const FilenameNodeVariable &node) override; +}; + +#endif // FILENAME_VISITOR_BASE_H diff --git a/lib/src/filename/ast/filename-visitor.h b/lib/src/filename/ast/filename-visitor.h new file mode 100644 index 000000000..e8aed605b --- /dev/null +++ b/lib/src/filename/ast/filename-visitor.h @@ -0,0 +1,35 @@ +#ifndef FILENAME_VISITOR_H +#define FILENAME_VISITOR_H + +struct FilenameNodeConditional; +struct FilenameNodeConditionIgnore; +struct FilenameNodeConditionInvert; +struct FilenameNodeConditionJavaScript; +struct FilenameNodeConditionOp; +struct FilenameNodeConditionTag; +struct FilenameNodeConditionToken; +struct FilenameNodeJavaScript; +struct FilenameNodeRoot; +struct FilenameNodeText; +struct FilenameNodeVariable; + + +class FilenameVisitor +{ + public: + virtual ~FilenameVisitor() = default; + + virtual void visit(const FilenameNodeConditional &node) = 0; + virtual void visit(const FilenameNodeConditionIgnore &node) = 0; + virtual void visit(const FilenameNodeConditionInvert &node) = 0; + virtual void visit(const FilenameNodeConditionJavaScript &node) = 0; + virtual void visit(const FilenameNodeConditionOp &node) = 0; + virtual void visit(const FilenameNodeConditionTag &node) = 0; + virtual void visit(const FilenameNodeConditionToken &node) = 0; + virtual void visit(const FilenameNodeJavaScript &node) = 0; + virtual void visit(const FilenameNodeRoot &node) = 0; + virtual void visit(const FilenameNodeText &node) = 0; + virtual void visit(const FilenameNodeVariable &node) = 0; +}; + +#endif // FILENAME_VISITOR_H diff --git a/lib/src/filename/conditional-filename.cpp b/lib/src/filename/conditional-filename.cpp new file mode 100644 index 000000000..cba94d200 --- /dev/null +++ b/lib/src/filename/conditional-filename.cpp @@ -0,0 +1,32 @@ +#include "filename/conditional-filename.h" +#include +#include "filename/filename-cache.h" +#include "filename/filename-condition-visitor.h" +#include "filename/filename-parser.h" +#include "logger.h" + + +ConditionalFilename::ConditionalFilename(QString condition, const QString &filename, QString path) + : condition(std::move(condition)), filename(filename), path(std::move(path)) +{ + if (!this->condition.isEmpty()) { + FilenameParser parser(this->condition); + FilenameNodeCondition *ast = parser.parseCondition(); + if (!parser.error().isEmpty()) { + log(QString("Error parsing condition '%1': %2").arg(this->condition, parser.error()), Logger::Error); + return; + } + + m_ast = ast; + } +} + +bool ConditionalFilename::matches(const QMap &tokens, QSettings *settings) const +{ + if (m_ast == nullptr) { + return false; + } + + FilenameConditionVisitor conditionVisitor(tokens, settings); + return conditionVisitor.run(*m_ast); +} diff --git a/lib/src/filename/conditional-filename.h b/lib/src/filename/conditional-filename.h new file mode 100644 index 000000000..9ccef90d3 --- /dev/null +++ b/lib/src/filename/conditional-filename.h @@ -0,0 +1,25 @@ +#ifndef CONDITIONAL_FILENAME_H +#define CONDITIONAL_FILENAME_H + +#include +#include +#include "models/filename.h" + + +struct FilenameNodeCondition; +class QSettings; +class Token; + +class ConditionalFilename +{ + public: + ConditionalFilename(QString condition, const QString &filename, QString path); + bool matches(const QMap &tokens, QSettings *settings) const; + + QString condition; + Filename filename; + QString path; + FilenameNodeCondition *m_ast = nullptr; +}; + +#endif // CONDITIONAL_FILENAME_H diff --git a/lib/src/filename/filename-cache.h b/lib/src/filename/filename-cache.h new file mode 100644 index 000000000..e8ed40655 --- /dev/null +++ b/lib/src/filename/filename-cache.h @@ -0,0 +1,12 @@ +#ifndef FILENAME_CACHE_H +#define FILENAME_CACHE_H + +#include "filename/ast-filename.h" +#include "flyweight-cache.h" + + +class FilenameCache : public FlyweightCache +{ +}; + +#endif // FILENAME_CACHE_H diff --git a/lib/src/filename/filename-condition-visitor.cpp b/lib/src/filename/filename-condition-visitor.cpp new file mode 100644 index 000000000..33c51243c --- /dev/null +++ b/lib/src/filename/filename-condition-visitor.cpp @@ -0,0 +1,78 @@ +#include "filename/filename-condition-visitor.h" +#include +#include +#include +#include "filename/ast/filename-node-condition.h" +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-node-condition-javascript.h" +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-javascript.h" +#include "functions.h" +#include "loader/token.h" +#include "logger.h" +#include "models/filtering/filter.h" + + +FilenameConditionVisitor::FilenameConditionVisitor(const QMap &tokens, QSettings *settings) + : FilenameVisitorJavaScript(settings), m_tokens(tokens) +{ + if (m_tokens.contains("allos")) { + m_tags = m_tokens["allos"].value().toStringList(); + } +} + +bool FilenameConditionVisitor::run(const FilenameNodeCondition &node) +{ + m_result = true; + + node.accept(*this); + + return m_result; +} + + +void FilenameConditionVisitor::visit(const FilenameNodeConditionInvert &node) +{ + node.node->accept(*this); + + m_result = !m_result; +} + +void FilenameConditionVisitor::visit(const FilenameNodeConditionJavaScript &node) +{ + QJSEngine engine; + setJavaScriptVariables(engine, m_tokens, engine.globalObject()); + + QJSValue result = engine.evaluate(node.script); + if (result.isError()) { + log("Error in Javascript evaluation:
" + result.toString()); + return; + } + + m_result = result.toBool(); +} + +void FilenameConditionVisitor::visit(const FilenameNodeConditionOp &node) +{ + node.left->accept(*this); + + // No need to evaluate the right operand in all cases + if ((!m_result && node.op == FilenameNodeConditionOp::And) + || (m_result && node.op == FilenameNodeConditionOp::Or)) { + return; + } + + node.right->accept(*this); +} + +void FilenameConditionVisitor::visit(const FilenameNodeConditionTag &node) +{ + m_result = node.filter->match(m_tokens).isEmpty(); +} + +void FilenameConditionVisitor::visit(const FilenameNodeConditionToken &node) +{ + m_result = m_tokens.contains(node.token) && !isVariantEmpty(m_tokens[node.token].value()); +} diff --git a/lib/src/filename/filename-condition-visitor.h b/lib/src/filename/filename-condition-visitor.h new file mode 100644 index 000000000..d268c7350 --- /dev/null +++ b/lib/src/filename/filename-condition-visitor.h @@ -0,0 +1,33 @@ +#ifndef FILENAME_CONDITION_VISITOR_H +#define FILENAME_CONDITION_VISITOR_H + +#include +#include +#include +#include "filename/filename-visitor-javascript.h" + + +struct FilenameNodeCondition; +class QSettings; +class Token; + +class FilenameConditionVisitor : public FilenameVisitorJavaScript +{ + public: + explicit FilenameConditionVisitor(const QMap &tokens, QSettings *settings); + bool run(const FilenameNodeCondition &node); + + void visit(const FilenameNodeConditionInvert &node) override; + void visit(const FilenameNodeConditionJavaScript &node) override; + void visit(const FilenameNodeConditionOp &node) override; + void visit(const FilenameNodeConditionTag &node) override; + void visit(const FilenameNodeConditionToken &node) override; + + private: + const QMap &m_tokens; + QStringList m_tags; + + bool m_result = false; +}; + +#endif // FILENAME_CONDITION_VISITOR_H diff --git a/lib/src/filename/filename-execution-visitor.cpp b/lib/src/filename/filename-execution-visitor.cpp new file mode 100644 index 000000000..d6763a0d6 --- /dev/null +++ b/lib/src/filename/filename-execution-visitor.cpp @@ -0,0 +1,264 @@ +#include "filename/filename-execution-visitor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "filename/ast/filename-node-condition-ignore.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-conditional.h" +#include "filename/ast/filename-node-javascript.h" +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-node-text.h" +#include "filename/ast/filename-node-variable.h" +#include "filename/filename-condition-visitor.h" +#include "loader/token.h" +#include "logger.h" +#include "models/image.h" + + +FilenameExecutionVisitor::FilenameExecutionVisitor(const QMap &tokens, QSettings *settings) + : FilenameVisitorJavaScript(settings), m_tokens(tokens), m_settings(settings) +{} + +void FilenameExecutionVisitor::setEscapeMethod(QString (*escapeMethod)(const QVariant &)) +{ + m_escapeMethod = escapeMethod; +} + +void FilenameExecutionVisitor::setKeepInvalidTokens(bool keepInvalidTokens) +{ + m_keepInvalidTokens = keepInvalidTokens; +} + + +QString FilenameExecutionVisitor::run(const FilenameNodeRoot &node) +{ + m_result.clear(); + + node.accept(*this); + + return m_result; +} + + +void FilenameExecutionVisitor::visit(const FilenameNodeConditional &node) +{ + FilenameConditionVisitor conditionVisitor(m_tokens, m_settings); + bool valid = conditionVisitor.run(*node.condition); + + if (valid && node.ifTrue != nullptr) { + node.ifTrue->accept(*this); + } else if (!valid && node.ifFalse != nullptr) { + node.ifFalse->accept(*this); + } +} + +void FilenameExecutionVisitor::visit(const FilenameNodeConditionIgnore &node) +{ + Q_UNUSED(node); + + // No-op +} + +void FilenameExecutionVisitor::visit(const FilenameNodeConditionTag &node) +{ + m_result += cleanVariable(node.tag.text()); +} + +void FilenameExecutionVisitor::visit(const FilenameNodeConditionToken &node) +{ + visitVariable(node.token); +} + +void FilenameExecutionVisitor::visit(const FilenameNodeJavaScript &node) +{ + QJSEngine engine; + setJavaScriptVariables(engine, m_tokens, engine.globalObject()); + + QJSValue result = engine.evaluate(node.script); + if (result.isError()) { + log("Error in Javascript evaluation:
" + result.toString()); + return; + } + + m_result += result.toString(); +} + +void FilenameExecutionVisitor::visit(const FilenameNodeText &node) +{ + m_result += node.text; +} + +void FilenameExecutionVisitor::visit(const FilenameNodeVariable &node) +{ + visitVariable(node.name, node.opts); +} + + +void FilenameExecutionVisitor::visitVariable(const QString &fullName, const QMap &options) +{ + // Contexts "obj.var" + bool found = true; + QStringList var = fullName.split('.'); + QString name = var.takeFirst(); + QMap context = m_tokens; + while (found && !var.isEmpty()) { + if (context.contains(name)) { + const QVariant &val = context[name].value(); + if (val.canConvert>()) { + context = val.value>(); + name = var.takeFirst(); + continue; + } + break; + } + found = false; + } + + // Variable not found + if (!found || !context.contains(name)) { + static const QSet keptTokens { "path", "num" }; + if (m_keepInvalidTokens || keptTokens.contains(name)) { + QString strOpts; + for (auto it = options.constBegin(); it != options.constEnd(); ++it) { + strOpts += it.key(); + if (!it.value().isEmpty()) { + strOpts += "=" + it.value(); + } + } + m_result += "%" + name + (!strOpts.isEmpty() ? ":" + strOpts : "") + "%"; // FIXME: get original value/proper toString() + } + return; + } + + QVariant val = context[name].value(); + QString res; + bool clean = false; + + // Convert value to a basic string using the given options + if (val.type() == QVariant::DateTime) { + res = variableToString(name, val.toDateTime(), options); + } else if (val.type() == QVariant::Int) { + res = variableToString(name, val.toInt(), options); + } else if (val.type() == QVariant::StringList) { + res = variableToString(name, val.toStringList(), options); + clean = true; + } else { + res = val.toString(); + } + + // String options + if (options.contains("maxlength")) { + res = res.left(options["maxlength"].toInt()); + } + if (options.contains("htmlescape")) { + res = res.toHtmlEscaped(); + } + + // Forbidden characters and spaces replacement settings + if (name != "allo" && !name.startsWith("url_") && name != "filename" && !clean) { + res = cleanVariable(res, options); + } + + // Escape if necessary + if (m_escapeMethod != nullptr && options.contains("escape")) { + res = m_escapeMethod(res); + } + + m_result += res; +} + +QString FilenameExecutionVisitor::variableToString(const QString &name, const QDateTime &val, const QMap &options) +{ + Q_UNUSED(name); + + const QString format = options.value("format", "MM-dd-yyyy HH.mm"); + return val.toString(format); +} + +QString FilenameExecutionVisitor::variableToString(const QString &name, int val, const QMap &options) +{ + Q_UNUSED(name); + + return options.contains("length") + ? QString("%1").arg(val, options["length"].toInt(), 10, QChar('0')) + : QString::number(val); +} + +QString FilenameExecutionVisitor::variableToString(const QString &name, QStringList val, const QMap &options) +{ + // Namespaces + bool ignoreNamespace = options.contains("ignorenamespace"); + bool includeNamespace = options.contains("includenamespace"); + if (ignoreNamespace || includeNamespace) { + QStringList namespaces = m_tokens["all_namespaces"].value().toStringList(); + + if (options.contains("ignorenamespace")) { + QStringList ignored = options["ignorenamespace"].split(' '); + QStringList filtered, filteredNamespaces; + for (int i = 0; i < val.count(); ++i) { + const QString &nspace = name == "all" ? namespaces[i] : name; + if (!ignored.contains(nspace)) { + filtered.append(val[i]); + filteredNamespaces.append(namespaces[i]); + } + } + val = filtered; + namespaces = filteredNamespaces; + } + if (options.contains("includenamespace")) { + QStringList excluded; + if (options.contains("excludenamespace")) { + excluded = options["excludenamespace"].split(' '); + } + + QStringList namespaced; + for (int i = 0; i < val.count(); ++i) { + const QString nspace = name == "all" ? namespaces[i] : name; + namespaced.append((!excluded.contains(nspace) ? nspace + ":" : QString()) + val[i]); + } + val = namespaced; + } + } + + if (options.contains("sort")) { + std::sort(val.begin(), val.end()); + } + + // Clean each value separately + if (!name.startsWith("source")) { + for (QString &t : val) { + t = cleanVariable(t, options); + } + } + + // Separator + QString mainSeparator = m_settings->value("Save/separator", " ").toString(); + QString tagSeparator = m_settings->value("Save/" + name + "_sep", mainSeparator).toString(); + QString separator = options.value("separator", tagSeparator); + separator.replace("\\n", "\n").replace("\\r", "\r"); + + return val.join(separator); +} + + +QString FilenameExecutionVisitor::cleanVariable(QString res, const QMap &options) const +{ + // Forbidden characters + if (!options.contains("unsafe")) { + res = res.replace("\\", "_").replace("%", "_").replace("/", "_").replace(":", "_").replace("|", "_").replace("*", "_").replace("?", "_").replace("\"", "_").replace("<", "_").replace(">", "_").replace("__", "_").replace("__", "_").replace("__", "_").trimmed(); + } + + // Replace underscores by spaces + if (!options.contains("underscores") && (!m_settings->value("Save/replaceblanks", false).toBool() || options.contains("spaces"))) { + res = res.replace("_", " "); + } + + return res; +} diff --git a/lib/src/filename/filename-execution-visitor.h b/lib/src/filename/filename-execution-visitor.h new file mode 100644 index 000000000..cc827b999 --- /dev/null +++ b/lib/src/filename/filename-execution-visitor.h @@ -0,0 +1,51 @@ +#ifndef FILENAME_EXECUTION_VISITOR_H +#define FILENAME_EXECUTION_VISITOR_H + +#include +#include +#include +#include +#include "filename/filename-visitor-javascript.h" + + +class QDateTime; +class QSettings; +class QStringList; +class QVariant; +class Token; + +class FilenameExecutionVisitor : public FilenameVisitorJavaScript +{ + public: + explicit FilenameExecutionVisitor(const QMap &tokens, QSettings *settings); + void setEscapeMethod(QString (*)(const QVariant &)); + void setKeepInvalidTokens(bool keepInvalidTokens); + + QString run(const FilenameNodeRoot &node); + + void visit(const FilenameNodeConditional &node) override; + void visit(const FilenameNodeConditionIgnore &node) override; + void visit(const FilenameNodeConditionTag &node) override; + void visit(const FilenameNodeConditionToken &node) override; + void visit(const FilenameNodeJavaScript &node) override; + void visit(const FilenameNodeText &node) override; + void visit(const FilenameNodeVariable &node) override; + + QString variableToString(const QString &name, const QDateTime &val, const QMap &options); + QString variableToString(const QString &name, int val, const QMap &options); + QString variableToString(const QString &name, QStringList val, const QMap &options); + + protected: + void visitVariable(const QString &name, const QMap &options = {}); + QString cleanVariable(QString val, const QMap &options = {}) const; + + private: + const QMap &m_tokens; + QSettings *m_settings; + QString (*m_escapeMethod)(const QVariant &) = nullptr; + bool m_keepInvalidTokens = false; + + QString m_result; +}; + +#endif // FILENAME_EXECUTION_VISITOR_H diff --git a/lib/src/filename/filename-parser.cpp b/lib/src/filename/filename-parser.cpp new file mode 100644 index 000000000..dcc4e5eb1 --- /dev/null +++ b/lib/src/filename/filename-parser.cpp @@ -0,0 +1,433 @@ +#include "filename/filename-parser.h" +#include +#include +#include +#include "filename/ast/filename-node-condition.h" +#include "filename/ast/filename-node-condition-ignore.h" +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-node-condition-javascript.h" +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-conditional.h" +#include "filename/ast/filename-node-javascript.h" +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-node-text.h" +#include "filename/ast/filename-node-variable.h" + +#define ESCAPE_CHARACTER '^' + + +FilenameParser::FilenameParser(QString str) + : m_str(std::move(str)), m_index(0) +{} + +const QString &FilenameParser::error() const +{ + return m_error; +} + + +FilenameNodeRoot *FilenameParser::parseRoot() +{ + try { + return parseRootNode(); + } catch (const std::runtime_error &e) { + m_error = e.what(); + return nullptr; + } +} + +FilenameNodeCondition *FilenameParser::parseCondition() +{ + try { + return parseConditionNode(); + } catch (const std::runtime_error &e) { + m_error = e.what(); + return nullptr; + } +} + + +QChar FilenameParser::peek() +{ + return m_str[m_index]; +} + +bool FilenameParser::finished() +{ + return m_index >= m_str.count(); +} + +void FilenameParser::skipSpaces() +{ + while (peek().isSpace()) { + m_index++; + } +} + +int FilenameParser::indexOf(const QList &chars, int max) +{ + bool escapeNext = false; + + int limit = max < 0 ? m_str.count() : qMin(max, m_str.count()); + for (int index = m_index; index < limit; ++index) { + QChar c = m_str[index]; + + // Don't return on escaped characters + bool isEscape = c == ESCAPE_CHARACTER || ((c == '<' || c == '>') && index < m_str.length() - 1 && c == m_str[index + 1]); + if (isEscape && !escapeNext) { + escapeNext = true; + } else if (chars.contains(c) && !escapeNext) { + return index; + } + + // Clear escape character if unused + if (!isEscape && escapeNext) { + escapeNext = false; + } + } + + return -1; +} + +QString FilenameParser::readUntil(const QList &chars, bool allowEnd) +{ + QString ret; + bool escapeNext = false; + + while (!finished()) { + QChar c = m_str[m_index]; + + // Don't return on escaped characters + bool isEscape = c == ESCAPE_CHARACTER || ((c == '<' || c == '>') && m_index < m_str.length() - 1 && c == m_str[m_index + 1]); + if (isEscape && !escapeNext) { + escapeNext = true; + } else { + if (chars.contains(c) && !escapeNext) { + return ret; + } + ret.append(c); + } + + // Clear escape character if unused + if (!isEscape && escapeNext) { + escapeNext = false; + } + + m_index++; + } + + if (!allowEnd) { + return QString(); + } + + return ret; +} + + +FilenameNodeRoot *FilenameParser::parseRootNode() +{ + QList exprs; + + while (!finished()) { + exprs.append(parseExpr()); + } + + return new FilenameNodeRoot(exprs); +} + +FilenameNode *FilenameParser::parseExpr(const QList &addChars) +{ + if (m_str.mid(m_index, 11) == "javascript:") { + return parseJavaScript(); + } + + QList until = QList{ '<', '%' } + addChars; + QString txt = readUntil(until, true); + if (txt.isEmpty()) { + QChar p = peek(); + + if (p == '<' && (m_index >= m_str.length() - 1 || m_str[m_index + 1] != '<')) { + return parseConditional(); + } + if (p == '%') { + return parseVariable(); + } + } + + return new FilenameNodeText(txt); +} + +FilenameNodeJavaScript *FilenameParser::parseJavaScript() +{ + m_index += 11; // javascript: + + int start = m_index; + m_index = m_str.length(); + + return new FilenameNodeJavaScript(m_str.mid(start)); +} + +FilenameNodeVariable *FilenameParser::parseVariable() +{ + m_index++; // % + + QString name = readUntil({ ':', '%' }); + + QMap opts; + while (peek() != '%') { + m_index++; // : or , + + QString opt = readUntil({ '=', ',', '%' }); + + QString val; + if (peek() == '=') { + m_index++; // = + val = readUntil({ ',', '%' }); + } + + opts.insert(opt, val); + } + + m_index++; // % + + return new FilenameNodeVariable(name, opts); +} + +FilenameNodeConditional *FilenameParser::parseConditional() +{ + m_index++; // < + + FilenameNodeCondition *condition = nullptr; + FilenameNode *ifTrue = nullptr; + FilenameNode *ifFalse = nullptr; + + // Legacy conditionals + int endIndex = indexOf({ '>' }); + int sepIndex = indexOf({ '?' }, endIndex); + if (sepIndex < 0) { + QList exprs; + QList conds; + + while (peek() != '>') { + QList stop { '>', '-', '!', '"', '%' }; + if (!stop.contains(peek())) { + exprs.append(parseExpr(stop)); + } + + if (peek() != '>') { + auto cond = parseSingleCondition(true); + conds.append(cond); + exprs.append(cond); + } + } + + if (conds.isEmpty()) { + throw std::runtime_error("No condition found in conditional"); + } + + condition = conds.takeFirst(); + while (!conds.isEmpty()) { + condition = new FilenameNodeConditionOp(FilenameNodeConditionOp::And, condition, conds.takeFirst()); + } + + ifTrue = exprs.count() == 1 + ? exprs.first() + : new FilenameNodeRoot(exprs); + } else { + condition = parseConditionNode(); + if (peek() != '?') { + delete condition; + throw std::runtime_error("Expected '?' after condition"); + } + m_index++; // ? + + ifTrue = parseExpr({ ':', '>' }); + if (peek() == ':') { + m_index++; // : + ifFalse = parseExpr({ '>' }); + } + + if (peek() != '>') { + delete condition; + delete ifTrue; + delete ifFalse; + throw std::runtime_error("Expected '>' at the end of contional"); + } + } + + m_index++; // > + + return new FilenameNodeConditional(condition, ifTrue, ifFalse); +} + +FilenameNodeCondition *FilenameParser::parseConditionNode() +{ + if (m_str.mid(m_index, 11) == "javascript:") { + return parseConditionJavaScript(); + } + + skipSpaces(); + + FilenameNodeCondition *lhs; + + // Parenthesis + QChar p = peek(); + if (p == '(') { + m_index++; // ( + + lhs = parseConditionNode(); + if (peek() != ')') { + delete lhs; + throw std::runtime_error("Expected ')' after condition in parenthesis"); + } + + m_index++; // ) + } else { + lhs = parseSingleCondition(); + } + + QStack opsStack; + QStack termStack; + + while (!finished()) { + skipSpaces(); + + QChar p = peek(); + QChar c = p == '"' || p == '%' ? '&' : p; + if (c != '&' && c != '|') { + break; + } + + termStack.push(lhs); + + int prec = (c == '&' ? 2 : 1); + + while (!opsStack.isEmpty()) { + QChar stackC = opsStack.top(); + int stackPrec = (stackC == '&' ? 2 : 1); + + if (prec > stackPrec) { + break; + } + + FilenameNodeCondition *operand2 = termStack.pop(); + FilenameNodeCondition *operand1 = termStack.pop(); + + QChar opC = opsStack.pop(); + auto op = opC == '&' ? FilenameNodeConditionOp::And : FilenameNodeConditionOp::Or; + auto expr = new FilenameNodeConditionOp(op, operand1, operand2); + termStack.push(expr); + } + + opsStack.push(c); + + if (p == c) { + m_index++; + skipSpaces(); + } + + termStack.push(parseSingleCondition()); + } + + while (!opsStack.isEmpty()) { + FilenameNodeCondition *operand2 = termStack.pop(); + FilenameNodeCondition *operand1 = termStack.pop(); + + QChar opC = opsStack.pop(); + auto op = opC == '&' ? FilenameNodeConditionOp::And : FilenameNodeConditionOp::Or; + auto expr = new FilenameNodeConditionOp(op, operand1, operand2); + termStack.push(expr); + } + + auto term = lhs; + + if (!termStack.isEmpty()) { + term = termStack.pop(); + } + + skipSpaces(); + + return term; +} + +FilenameNodeCondition *FilenameParser::parseSingleCondition(bool legacy) +{ + QChar c = peek(); + + if (legacy && c == '-') { + return parseConditionIgnore(); + } + if (c == '!' || c == '-') { + return parseConditionInvert(); + } + if (c == '%') { + return parseConditionToken(); + } + if (c == '"') { + return parseConditionTag(); + } + + if (!legacy) { + return parseConditionTag(false); + } + + throw std::runtime_error("Expected '!', '%' or '\"' for condition"); +} + +FilenameNodeConditionIgnore *FilenameParser::parseConditionIgnore() +{ + m_index++; // - + + auto cond = parseSingleCondition(); + + return new FilenameNodeConditionIgnore(cond); +} + +FilenameNodeConditionInvert *FilenameParser::parseConditionInvert() +{ + m_index++; // ! or - + + auto cond = parseSingleCondition(); + + return new FilenameNodeConditionInvert(cond); +} + +FilenameNodeConditionJavaScript *FilenameParser::parseConditionJavaScript() +{ + m_index += 11; // javascript: + + int start = m_index; + m_index = m_str.length(); + + return new FilenameNodeConditionJavaScript(m_str.mid(start)); +} + +FilenameNodeConditionTag *FilenameParser::parseConditionTag(bool quotes) +{ + if (quotes) { + m_index++; // " + } + + QString tag = quotes + ? readUntil({ '"' }) + : readUntil({ ' ', '&', '|', '?' }, true); + + if (quotes) { + m_index++; // " + } + + return new FilenameNodeConditionTag(Tag(tag)); +} + +FilenameNodeConditionToken *FilenameParser::parseConditionToken() +{ + m_index++; // % + + QString token = readUntil({ '%' }); + + m_index++; // % + + return new FilenameNodeConditionToken(token); +} diff --git a/lib/src/filename/filename-parser.h b/lib/src/filename/filename-parser.h new file mode 100644 index 000000000..b29a1f1ce --- /dev/null +++ b/lib/src/filename/filename-parser.h @@ -0,0 +1,56 @@ +#ifndef FILENAME_PARSER_H +#define FILENAME_PARSER_H + +#include +#include +#include + + +struct FilenameNode; +struct FilenameNodeCondition; +struct FilenameNodeConditional; +struct FilenameNodeConditionIgnore; +struct FilenameNodeConditionInvert; +struct FilenameNodeConditionJavaScript; +struct FilenameNodeConditionTag; +struct FilenameNodeConditionToken; +struct FilenameNodeJavaScript; +struct FilenameNodeRoot; +struct FilenameNodeVariable; + +class FilenameParser +{ + public: + explicit FilenameParser(QString str); + const QString &error() const; + + FilenameNodeRoot *parseRoot(); + FilenameNodeCondition *parseCondition(); + FilenameNodeVariable *parseVariable(); + + protected: + QChar peek(); + bool finished(); + void skipSpaces(); + int indexOf(const QList &chars, int max = -1); + QString readUntil(const QList &chars, bool allowEnd = false); + + FilenameNodeRoot *parseRootNode(); + FilenameNode *parseExpr(const QList &addChars = {}); + FilenameNodeJavaScript *parseJavaScript(); + FilenameNodeConditional *parseConditional(); + FilenameNodeCondition *parseConditionNode(); + FilenameNodeCondition *parseSingleCondition(bool legacy = false); + FilenameNodeConditionIgnore *parseConditionIgnore(); + FilenameNodeConditionJavaScript *parseConditionJavaScript(); + FilenameNodeConditionInvert *parseConditionInvert(); + FilenameNodeConditionTag *parseConditionTag(bool quotes = true); + FilenameNodeConditionToken *parseConditionToken(); + + private: + QString m_str; + int m_index; + QString m_error; +}; + +#endif // FILENAME_PARSER_H diff --git a/lib/src/filename/filename-resolution-visitor.cpp b/lib/src/filename/filename-resolution-visitor.cpp new file mode 100644 index 000000000..ad27426d6 --- /dev/null +++ b/lib/src/filename/filename-resolution-visitor.cpp @@ -0,0 +1,25 @@ +#include "filename/filename-resolution-visitor.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-node-variable.h" + + +QSet FilenameResolutionVisitor::run(const FilenameNodeRoot &node) +{ + m_results.clear(); + + node.accept(*this); + + return m_results; +} + + +void FilenameResolutionVisitor::visit(const FilenameNodeConditionToken &node) +{ + m_results.insert(node.token); +} + +void FilenameResolutionVisitor::visit(const FilenameNodeVariable &node) +{ + m_results.insert(node.name); +} diff --git a/lib/src/filename/filename-resolution-visitor.h b/lib/src/filename/filename-resolution-visitor.h new file mode 100644 index 000000000..64bf9b603 --- /dev/null +++ b/lib/src/filename/filename-resolution-visitor.h @@ -0,0 +1,21 @@ +#ifndef FILENAME_RESOLUTION_VISITOR_H +#define FILENAME_RESOLUTION_VISITOR_H + +#include +#include +#include "filename/ast/filename-visitor-base.h" + + +class FilenameResolutionVisitor : public FilenameVisitorBase +{ + public: + QSet run(const FilenameNodeRoot &node); + + void visit(const FilenameNodeConditionToken &node) override; + void visit(const FilenameNodeVariable &node) override; + + private: + QSet m_results; +}; + +#endif // FILENAME_RESOLUTION_VISITOR_H diff --git a/lib/src/filename/filename-visitor-javascript.cpp b/lib/src/filename/filename-visitor-javascript.cpp new file mode 100644 index 000000000..3c9729553 --- /dev/null +++ b/lib/src/filename/filename-visitor-javascript.cpp @@ -0,0 +1,67 @@ +#include "filename/filename-visitor-javascript.h" +#include +#include +#include +#include +#include "loader/token.h" +#include "models/image.h" +#include "models/profile.h" + + +FilenameVisitorJavaScript::FilenameVisitorJavaScript(QSettings *settings) + : m_settings(settings) +{} + + +void FilenameVisitorJavaScript::setJavaScriptVariables(QJSEngine &engine, const QMap &tokens, QJSValue obj) const +{ + for (auto it = tokens.constBegin(); it != tokens.constEnd(); ++it) { + const QString &name = it.key(); + QVariant val = it.value().value(); + + if (val.type() == QVariant::StringList || val.type() == QVariant::String) { + QString res; + + if (val.type() == QVariant::StringList) { + QStringList vals = val.toStringList(); + if (name != "all" && name != "tags") { + obj.setProperty(name + "s", engine.toScriptValue(vals)); + } + res = vals.join(separator(name, QString())); + } else { + res = val.toString(); + } + + if (name != "allo") { + res = res.replace("\\", "_").replace("%", "_").replace("/", "_").replace(":", "_").replace("|", "_").replace("*", "_").replace("?", "_").replace("\"", "_").replace("<", "_").replace(">", "_").replace("__", "_").replace("__", "_").replace("__", "_").trimmed(); + if (!m_settings->value("Save/replaceblanks", false).toBool()) { + res.replace("_", " "); + } + } + + obj.setProperty(name, res); + } else if (val.canConvert>()) { + QJSValue v = engine.newObject(); + QMap subTokens = val.value>(); + setJavaScriptVariables(engine, subTokens, v); + obj.setProperty(name, v); + } else { + obj.setProperty(name, engine.toScriptValue(val)); + } + } +} + +QString FilenameVisitorJavaScript::separator(const QString &key, const QString &override) const +{ + QString separator; + + if (!override.isEmpty()) { + separator = override; + } else { + QString mainSeparator = m_settings->value("Save/separator", " ").toString(); + separator = m_settings->value("Save/" + key + "_sep", mainSeparator).toString(); + } + + separator.replace("\\n", "\n").replace("\\r", "\r"); + return separator; +} diff --git a/lib/src/filename/filename-visitor-javascript.h b/lib/src/filename/filename-visitor-javascript.h new file mode 100644 index 000000000..5331e11e6 --- /dev/null +++ b/lib/src/filename/filename-visitor-javascript.h @@ -0,0 +1,27 @@ +#ifndef FILENAME_VISITOR_JAVASCRIPT_H +#define FILENAME_VISITOR_JAVASCRIPT_H + +#include +#include +#include +#include +#include "filename/ast/filename-visitor-base.h" + + +class QSettings; +class Token; + +class FilenameVisitorJavaScript : public FilenameVisitorBase +{ + public: + explicit FilenameVisitorJavaScript(QSettings *settings); + + protected: + void setJavaScriptVariables(QJSEngine &engine, const QMap &tokens, QJSValue obj) const; + QString separator(const QString &key, const QString &override) const; + + private: + QSettings *m_settings; +}; + +#endif // FILENAME_VISITOR_JAVASCRIPT_H diff --git a/lib/src/flyweight-cache.h b/lib/src/flyweight-cache.h index 51e84728c..ce4a3aee2 100644 --- a/lib/src/flyweight-cache.h +++ b/lib/src/flyweight-cache.h @@ -3,6 +3,7 @@ #include #include +#include template @@ -23,8 +24,7 @@ template QSharedPointer FlyweightCache::Get(const K &key) { auto it = Map.find(key); - if (it != Map.end()) - { + if (it != Map.end() && !it.value().isNull()) { return QSharedPointer(it.value()); } diff --git a/lib/src/functions.cpp b/lib/src/functions.cpp index 7f079bc02..e6d7b7486 100644 --- a/lib/src/functions.cpp +++ b/lib/src/functions.cpp @@ -3,10 +3,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -23,6 +23,7 @@ #ifdef QT_DEBUG #include #endif +#include "logger.h" #include "vendor/html-entities.h" @@ -35,8 +36,9 @@ QMap getCustoms(QSettings *settings) QMap tokens; settings->beginGroup(QStringLiteral("Save/Customs")); const QStringList keys = settings->childKeys(); - for (const QString &key : keys) - { tokens.insert(key, settings->value(key).toString().split(' ')); } + for (const QString &key : keys) { + tokens.insert(key, settings->value(key).toString().split(' ')); + } settings->endGroup(); return tokens; } @@ -45,25 +47,25 @@ QMap getCustoms(QSettings *settings) * Load multiple filenames from settings. * @return The map with token names as keys and token tags as values. */ -QMap> getFilenames(QSettings *settings) +QList getFilenames(QSettings *settings) { - QMap> tokens; + QList ret; settings->beginGroup(QStringLiteral("Filenames")); const int count = settings->childKeys().count() / 3; - for (int i = 0; i < count; i++) - { - if (settings->contains(QString::number(i) + "_cond")) - { - QPair pair; - pair.first = settings->value(QString::number(i) + "_fn").toString(); - pair.second = settings->value(QString::number(i) + "_dir").toString(); - tokens.insert(settings->value(QString::number(i) + "_cond").toString(), pair); + for (int i = 0; i < count; i++) { + const QString strI = QString::number(i); + if (settings->contains(strI + "_cond")) { + ret.append(ConditionalFilename( + settings->value(strI + "_cond").toString(), + settings->value(strI + "_fn").toString(), + settings->value(strI + "_dir").toString() + )); } } settings->endGroup(); - return tokens; + return ret; } QMap> getExternalLogFiles(QSettings *settings) @@ -71,12 +73,12 @@ QMap> getExternalLogFiles(QSettings *settings) QMap> ret; settings->beginGroup(QStringLiteral("LogFiles")); - for (const QString &group : settings->childGroups()) - { + for (const QString &group : settings->childGroups()) { settings->beginGroup(group); QMap logSettings; - for (const QString &key : settings->childKeys()) - { logSettings.insert(key, settings->value(key)); } + for (const QString &key : settings->childKeys()) { + logSettings.insert(key, settings->value(key)); + } ret.insert(group.toInt(), logSettings); settings->endGroup(); } @@ -89,11 +91,11 @@ QStringList getExternalLogFilesSuffixes(QSettings *settings) QStringList suffixes; auto logFiles = getExternalLogFiles(settings); - for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) - { + for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) { const QMap &logFile = it.value(); - if (logFile["locationType"].toInt() == 2) - { suffixes.append(logFile["suffix"].toString()); } + if (logFile["locationType"].toInt() == 2) { + suffixes.append(logFile["suffix"].toString()); + } } return suffixes; @@ -106,21 +108,19 @@ QStringList removeWildards(const QStringList &elements, const QStringList &remov QRegExp reg; reg.setCaseSensitivity(Qt::CaseInsensitive); reg.setPatternSyntax(QRegExp::Wildcard); - for (const QString &tag : elements) - { + for (const QString &tag : elements) { bool removed = false; - for (const QString &rem : remove) - { + for (const QString &rem : remove) { reg.setPattern(rem); - if (reg.exactMatch(tag)) - { + if (reg.exactMatch(tag)) { removed = true; break; } } - if (!removed) + if (!removed) { tags.append(tag); + } } return tags; @@ -136,43 +136,37 @@ QDateTime qDateTimeFromString(const QString &str) QDateTime date; const uint toInt = str.toUInt(); - if (toInt != 0) - { + if (toInt != 0) { date.setTime_t(toInt); - } - else if (str.length() == 19) - { + } else if (str.length() == 19) { date = QDateTime::fromString(str, QStringLiteral("yyyy/MM/dd HH:mm:ss")); - if (!date.isValid()) + if (!date.isValid()) { date = QDateTime::fromString(str, QStringLiteral("yyyy-MM-dd HH:mm:ss")); + } date.setTimeSpec(Qt::UTC); - } - else if (str.length() == 16) - { + } else if (str.length() == 16) { date = QDateTime::fromString(str, QStringLiteral("yyyy/MM/dd HH:mm")); - if (!date.isValid()) + if (!date.isValid()) { date = QDateTime::fromString(str, QStringLiteral("yyyy-MM-dd HH:mm")); + } date.setTimeSpec(Qt::UTC); - } - else if (str[0].isDigit()) - { + } else if (str[0].isDigit()) { qreal decay = 0; date = QDateTime::fromString(str.left(19), QStringLiteral("yyyy-MM-dd'T'HH:mm:ss")); - if (!date.isValid()) + if (!date.isValid()) { date = QDateTime::fromString(str.left(19), QStringLiteral("yyyy/MM/dd HH:mm:ss")); - else + } else { decay = str.right(6).remove(':').toDouble() / 100; + } date.setOffsetFromUtc(qFloor(3600 * decay)); - } - else - { + } else { QLocale myLoc(QLocale::English); date = myLoc.toDateTime(str, QStringLiteral("ddd MMM dd HH:mm:ss yyyy")); - if (!date.isValid()) + if (!date.isValid()) { date = myLoc.toDateTime(str, QStringLiteral("ddd MMM d HH:mm:ss yyyy")); - if (date.isValid()) - { + } + if (date.isValid()) { date.setTimeSpec(Qt::UTC); return date; } @@ -198,8 +192,7 @@ QString getUnit(double *size) const int multiplier = FILESIZE_MULTIPLIER; int power = 0; - while (*size >= multiplier && power < units.count() - 1) - { + while (*size >= multiplier && power < units.count() - 1) { *size /= 1024; power++; } @@ -233,28 +226,46 @@ QString savePath(const QString &file, bool exists, bool writable) { const QString &check = exists ? file : QStringLiteral("settings.ini"); - if (isTestModeEnabled()) - { - if (QDir(QDir::currentPath() + "/tests/resources/").exists()) - { return QDir::toNativeSeparators(QDir::currentPath() + "/tests/resources/" + file); } + if (isTestModeEnabled()) { + if (QDir(QDir::currentPath() + "/tests/resources/").exists()) { + return QDir::toNativeSeparators(QDir::currentPath() + "/tests/resources/" + file); + } } - if (validSavePath(qApp->applicationDirPath() + "/" + check, writable)) - { return QDir::toNativeSeparators(qApp->applicationDirPath() + "/" + file); } - if (validSavePath(QDir::currentPath() + "/" + check, writable)) - { return QDir::toNativeSeparators(QDir::currentPath() + "/" + file); } - if (validSavePath(QDir::homePath() + "/Grabber/" + check, writable)) - { return QDir::toNativeSeparators(QDir::homePath() + "/Grabber/" + file); } + if (validSavePath(qApp->applicationDirPath() + "/" + check, writable)) { + return QDir::toNativeSeparators(qApp->applicationDirPath() + "/" + file); + } + if (validSavePath(QDir::currentPath() + "/" + check, writable)) { + return QDir::toNativeSeparators(QDir::currentPath() + "/" + file); + } + if (validSavePath(QDir::homePath() + "/Grabber/" + check, writable)) { + return QDir::toNativeSeparators(QDir::homePath() + "/Grabber/" + file); + } + #if defined(Q_OS_ANDROID) + const QString &appData = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (validSavePath(appData + "/" + check, writable)) { + return QDir::toNativeSeparators(appData + "/" + file); + } + if (validSavePath("assets:/" + check, writable)) { + return QDir::toNativeSeparators("assets:/" + file); + } + #endif #ifdef __linux__ - if (validSavePath(QDir::homePath() + "/.Grabber/" + check, writable)) - { return QDir::toNativeSeparators(QDir::homePath() + "/.Grabber/" + file); } - if (validSavePath(QString(PREFIX) + "/share/Grabber/" + check, writable)) - { return QDir::toNativeSeparators(QString(PREFIX) + "/share/Grabber/" + file); } + if (validSavePath(QDir::homePath() + "/.Grabber/" + check, writable)) { + return QDir::toNativeSeparators(QDir::homePath() + "/.Grabber/" + file); + } + if (validSavePath(QString(PREFIX) + "/share/Grabber/" + check, writable)) { + return QDir::toNativeSeparators(QString(PREFIX) + "/share/Grabber/" + file); + } #endif QString dir; #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) - dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + #if defined(Q_OS_ANDROID) + dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + #else + dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + #endif #else dir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); #ifdef __linux__ @@ -268,29 +279,33 @@ QString savePath(const QString &file, bool exists, bool writable) bool copyRecursively(QString srcFilePath, QString tgtFilePath) { // Trim directory names of their trailing slashes - if (srcFilePath.endsWith(QDir::separator())) + if (srcFilePath.endsWith(QDir::separator())) { srcFilePath.chop(1); - if (tgtFilePath.endsWith(QDir::separator())) + } + if (tgtFilePath.endsWith(QDir::separator())) { tgtFilePath.chop(1); + } // Directly copy files using Qt function - if (!QFileInfo(srcFilePath).isDir()) + if (!QFileInfo(srcFilePath).isDir()) { return QFile(srcFilePath).copy(tgtFilePath); + } // Try to create the target directory QDir targetDir(tgtFilePath); targetDir.cdUp(); - if (!targetDir.mkdir(QDir(tgtFilePath).dirName())) + if (!targetDir.mkdir(QDir(tgtFilePath).dirName())) { return false; + } QDir sourceDir(srcFilePath); QStringList fileNames = sourceDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); - for (const QString &fileName : fileNames) - { + for (const QString &fileName : fileNames) { const QString newSrcFilePath = srcFilePath + QDir::separator() + fileName; const QString newTgtFilePath = tgtFilePath + QDir::separator() + fileName; - if (!copyRecursively(newSrcFilePath, newTgtFilePath)) + if (!copyRecursively(newSrcFilePath, newTgtFilePath)) { return false; + } } return true; @@ -308,15 +323,15 @@ int levenshtein(QString s1, QString s2) QVector> d(len1 + 1, QVector(len2 + 1)); d[0][0] = 0; - for (int i = 1; i <= len1; ++i) + for (int i = 1; i <= len1; ++i) { d[i][0] = i; - for (int i = 1; i <= len2; ++i) + } + for (int i = 1; i <= len2; ++i) { d[0][i] = i; + } - for (int i = 1; i <= len1; ++i) - { - for (int j = 1; j <= len2; ++j) - { + for (int i = 1; i <= len1; ++i) { + for (int j = 1; j <= len2; ++j) { const int a = qMin(d[i - 1][j] + 1, d[i][j - 1] + 1); const int b = d[i - 1][j - 1] + (s1[i - 1] == s2[j - 1] ? 0 : 1); d[i][j] = qMin(a, b); @@ -328,16 +343,16 @@ int levenshtein(QString s1, QString s2) bool setFileCreationDate(const QString &path, const QDateTime &datetime) { - if (!datetime.isValid()) - { return false; } + if (!datetime.isValid()) { + return false; + } #ifdef Q_OS_WIN auto *filename = new wchar_t[path.length() + 1]; path.toWCharArray(filename); filename[path.length()] = 0; HANDLE hfile = CreateFileW(filename, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); delete[] filename; - if (hfile == INVALID_HANDLE_VALUE) - { + if (hfile == INVALID_HANDLE_VALUE) { log(QStringLiteral("Unable to open file to set creation date (%1): %2").arg(GetLastError()).arg(path), Logger::Error); return false; } @@ -347,8 +362,7 @@ bool setFileCreationDate(const QString &path, const QDateTime &datetime) pcreationtime.dwLowDateTime = static_cast(ll); pcreationtime.dwHighDateTime = ll >> 32; - if (!SetFileTime(hfile, &pcreationtime, nullptr, &pcreationtime)) - { + if (SetFileTime(hfile, &pcreationtime, nullptr, &pcreationtime) == FALSE) { log(QStringLiteral("Unable to change the file creation date (%1): %2").arg(GetLastError()).arg(path), Logger::Error); return false; } @@ -358,8 +372,7 @@ bool setFileCreationDate(const QString &path, const QDateTime &datetime) struct utimbuf timebuffer; timebuffer.modtime = datetime.toTime_t(); const char *filename = path.toStdString().c_str(); - if ((utime(filename, &timebuffer)) < 0) - { + if ((utime(filename, &timebuffer)) < 0) { log(QStringLiteral("Unable to change the file creation date (%1): %2").arg(errno).arg(path), Logger::Error); return false; } @@ -367,30 +380,6 @@ bool setFileCreationDate(const QString &path, const QDateTime &datetime) return true; } -/** - * Converts a DOM elemet to a map. - * @param dom The DOM element to convert. - * @return A QString map with names (joined with a slash if necessary) as keys and texts as values. - */ -QMap domToMap(const QDomElement &dom) -{ - QMap details; - for (QDomNode n = dom.firstChild(); !n.isNull(); n = n.nextSibling()) - { - const auto type = n.firstChild().nodeType(); - if (type == QDomNode::TextNode || type == QDomNode::CDATASectionNode) - { details[n.nodeName()] = n.firstChild().nodeValue(); } - else - { - QMap r = domToMap(n.toElement()); - QStringList k = r.keys(); - for (int i = 0; i < r.count(); i++) - { details[n.nodeName() + "/" + k.at(i)] = r.value(k.at(i)); } - } - } - return details; -} - /** * Removes HTML from a string. * @param str The string to remove HTML from. @@ -432,8 +421,9 @@ QString getExtension(const QUrl &url) const QString filename = url.fileName(); const int lastDot = filename.lastIndexOf('.'); - if (lastDot != -1) + if (lastDot != -1) { return filename.mid(lastDot + 1); + } return QString(); } @@ -444,8 +434,9 @@ QUrl setExtension(QUrl url, const QString &extension) const int lastSlash = path.lastIndexOf('/'); const int lastDot = path.midRef(lastSlash + 1).lastIndexOf('.'); - if (lastDot != -1) + if (lastDot != -1) { url.setPath(path.left(lastDot + lastSlash + 1) + "." + extension); + } return url; } @@ -461,8 +452,9 @@ QString fixFilename(QString filename, QString path, int maxLength, bool invalidC const QString sep = QDir::separator(); filename = QDir::toNativeSeparators(filename); path = QDir::toNativeSeparators(path); - if (!path.endsWith(sep) && !path.isEmpty() && !filename.isEmpty()) + if (!path.endsWith(sep) && !path.isEmpty() && !filename.isEmpty()) { path += sep; + } #ifdef Q_OS_WIN return fixFilenameWindows(filename, path, maxLength, invalidChars); @@ -482,26 +474,24 @@ QString fixFilenameLinux(const QString &fn, const QString &path, int maxLength, // Divide filename QStringList parts = filename.split(sep); QString file, ext; - if (!fn.isEmpty()) - { + if (!fn.isEmpty()) { file = parts.takeLast();; const int lastDot = file.lastIndexOf('.'); - if (lastDot != -1) - { + if (lastDot != -1) { ext = file.right(file.length() - lastDot - 1); file = file.left(lastDot); } } // Fix directories - for (QString &part : parts) - { + for (QString &part : parts) { // A part cannot start or finish with a space part = part.trimmed(); // Trim part - if (part.length() > 255) + if (part.length() > 255) { part = part.left(255).trimmed(); + } } // Join parts back @@ -510,21 +500,25 @@ QString fixFilenameLinux(const QString &fn, const QString &path, int maxLength, // A filename cannot exceed a certain length const int extlen = ext.isEmpty() ? 0 : ext.length() + 1; - if (file.length() > maxLength - extlen) + if (file.length() > maxLength - extlen) { file = file.left(maxLength - extlen).trimmed(); - if (file.length() > 255 - extlen) + } + if (file.length() > 255 - extlen) { file = file.left(255 - extlen).trimmed(); + } // Get separation between filename and path int index = -1; const int pathGroups = path.count(sep); - for (int i = 0; i < pathGroups; ++i) + for (int i = 0; i < pathGroups; ++i) { index = filename.indexOf(sep, index + 1); + } // Put extension and drive back filename = filename + (!ext.isEmpty() ? "." + ext : QString()); - if (!fn.isEmpty()) + if (!fn.isEmpty()) { filename = filename.right(filename.length() - index - 1); + } QFileInfo fi(filename); QString suffix = fi.suffix(); @@ -546,15 +540,15 @@ QString fixFilenameWindows(const QString &fn, const QString &path, int maxLength // Drive QString drive; - if (filename.mid(1, 2) == QLatin1String(":\\")) - { + if (filename.mid(1, 2) == QLatin1String(":\\")) { drive = filename.left(3); filename = filename.right(filename.length() - 3); } // Forbidden characters - if (invalidChars) - { filename.replace('<', '_').replace('>', '_').replace(':', '_').remove('"').replace('/', '_').replace('|', '_').remove('?').replace('*', '_'); } + if (invalidChars) { + filename.replace('<', '_').replace('>', '_').replace(':', '_').remove('"').replace('/', '_').replace('|', '_').remove('?').replace('*', '_'); + } // Fobidden directories or filenames static const QStringList forbidden = QStringList() << "CON" << "PRN" << "AUX" << "NUL" << "COM1" << "COM2" << "COM3" << "COM4" << "COM5" << "COM6" << "COM7" << "COM8" << "COM9" << "LPT1" << "LPT2" << "LPT3" << "LPT4" << "LPT5" << "LPT6" << "LPT7" << "LPT8" << "LPT9"; @@ -562,57 +556,61 @@ QString fixFilenameWindows(const QString &fn, const QString &path, int maxLength // Divide filename QStringList parts = filename.split(sep); QString file, ext; - if (!fn.isEmpty()) - { + if (!fn.isEmpty()) { file = parts.takeLast(); const int lastDot = file.lastIndexOf('.'); - if (lastDot != -1) - { + if (lastDot != -1) { ext = file.right(file.length() - lastDot - 1); file = file.left(lastDot); } } // Fix directories - for (QString &part : parts) - { + for (QString &part : parts) { // A part cannot be one in the forbidden list - if (invalidChars && forbidden.contains(part)) - { part = part + "!"; } + if (invalidChars && forbidden.contains(part)) { + part = part + "!"; + } // A part cannot finish by a period - if (invalidChars && part.endsWith('.')) - { part = part.left(part.length() - 1).trimmed(); } + while (invalidChars && part.endsWith('.')) { + part = part.left(part.length() - 1).trimmed(); + } // A part cannot start or finish with a space part = part.trimmed(); // A part should still allow creating a file - if (part.length() > maxLength - 12) - { part = part.left(qMax(0, maxLength - 12)).trimmed(); } + if (part.length() > maxLength - 12) { + part = part.left(qMax(0, maxLength - 12)).trimmed(); + } } // Join parts back QString dirpart = parts.join(sep); - if (dirpart.length() > maxLength - 12) - { dirpart = dirpart.left(qMax(0, maxLength - 12)).trimmed(); } + if (dirpart.length() > maxLength - 12) { + dirpart = dirpart.left(qMax(0, maxLength - 12)).trimmed(); + } filename = (dirpart.isEmpty() ? QString() : dirpart + (!fn.isEmpty() ? sep : QString())) + file; // A filename cannot exceed MAX_PATH (-1 for and -3 for drive "C:\") - if (filename.length() > maxLength - 1 - 3 - ext.length() - 1) - { filename = filename.left(qMax(0, maxLength - 1 - 3 - ext.length() - 1)).trimmed(); } + if (filename.length() > maxLength - 1 - 3 - ext.length() - 1) { + filename = filename.left(qMax(0, maxLength - 1 - 3 - ext.length() - 1)).trimmed(); + } // Get separation between filename and path int index = -1; const int pathGroups = path.count(sep); - for (int i = 0; i < pathGroups - (!drive.isEmpty() ? 1 : 0); ++i) - { index = filename.indexOf(sep, index + 1); } + for (int i = 0; i < pathGroups - (!drive.isEmpty() ? 1 : 0); ++i) { + index = filename.indexOf(sep, index + 1); + } index += drive.length(); // Put extension and drive back filename = drive + filename + (!ext.isEmpty() ? "." + ext : QString()); - if (!fn.isEmpty()) - { filename = filename.right(filename.length() - index - 1); } + if (!fn.isEmpty()) { + filename = filename.right(filename.length() - index - 1); + } return filename; } @@ -628,40 +626,49 @@ QString getExtensionFromHeader(const QByteArray &data12) const QByteArray data2 = data12.left(2); // GIF - if (data6 == "GIF87a" || data6 == "GIF89a") + if (data6 == "GIF87a" || data6 == "GIF89a") { return QStringLiteral("gif"); + } // PNG - if (data8 == "\211PNG\r\n\032\n") + if (data8 == "\211PNG\r\n\032\n") { return QStringLiteral("png"); + } // JPG - if (data3 == "\377\330\377") + if (data3 == "\377\330\377") { return QStringLiteral("jpg"); + } // BMP - if (data2 == "BM") + if (data2 == "BM") { return QStringLiteral("bmp"); + } // WEBM - if (data4 == "\032\105\337\243") + if (data4 == "\032\105\337\243") { return QStringLiteral("webm"); + } // MP4 - if (data48 == "ftyp3gp5" || data48 == "ftypMSNV" || data48 == "ftypisom") + if (data48 == "ftyp3gp5" || data48 == "ftypMSNV" || data48 == "ftypisom") { return QStringLiteral("mp4"); + } // SWF - if (data3 == "FWS" || data3 == "CWS" || data3 == "ZWS") + if (data3 == "FWS" || data3 == "CWS" || data3 == "ZWS") { return QStringLiteral("swf"); + } // FLV - if (data4 == "FLV\001") + if (data4 == "FLV\001") { return QStringLiteral("flv"); + } // ICO - if (data4 == QByteArray("\000\000\001\000", 4)) + if (data4 == QByteArray("\000\000\001\000", 4)) { return QStringLiteral("ico"); + } return QString(); } @@ -682,8 +689,7 @@ QString fixCloudflareEmail(const QString &a) { QString s; int r = a.midRef(0, 2).toInt(nullptr, 16); - for (int j = 2; a.length() - j; j += 2) - { + for (int j = 2; j < a.length(); j += 2) { int c = a.midRef(j, 2).toInt(nullptr, 16) ^ r; s += QString(QChar(c)); } @@ -693,8 +699,7 @@ QString fixCloudflareEmails(QString html) { static QRegularExpression rx("\\[[^<]+\\]<\\/span>"); auto matches = rx.globalMatch(html); - while (matches.hasNext()) - { + while (matches.hasNext()) { auto match = matches.next(); const QString email = fixCloudflareEmail(match.captured(1)); html.replace(match.captured(0), email); @@ -706,14 +711,16 @@ QString fixCloudflareEmails(QString html) QString getFileMd5(const QString &path) { QFile file(path); - file.open(QFile::ReadOnly); + if (!file.open(QFile::ReadOnly)) { + return QString(); + } return QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5).toHex(); } QString getFilenameMd5(const QString &fileName, const QString &format) { QRegularExpression regx("%([^%]*)%"); - QString reg = QRegExp::escape(format); + QString reg = "^" + QRegExp::escape(format) + "$"; auto matches = regx.globalMatch(format); while (matches.hasNext()) { const auto match = matches.next(); @@ -746,8 +753,7 @@ QString parseMarkdown(QString str) // Headers static const QRegularExpression header(QStringLiteral("^(#+)([^#].*)$"), QRegularExpression::MultilineOption); auto matches = header.globalMatch(str); - while (matches.hasNext()) - { + while (matches.hasNext()) { auto match = matches.next(); const int level = qMax(1, qMin(6, match.captured(1).length())); const QString result = "" + match.captured(2).trimmed() + ""; @@ -781,17 +787,22 @@ QString qFontToCss(const QFont &font) } QString size; - if (font.pixelSize() == -1) - { size = QString::number(font.pointSize()) + "pt"; } - else - { size = QString::number(font.pixelSize()) + "px"; } + if (font.pixelSize() == -1) { + size = QString::number(font.pointSize()) + "pt"; + } else { + size = QString::number(font.pixelSize()) + "px"; + } // Should be "font.weight() * 8 + 100", but linux doesn't handle weight the same way windows do const QString weight = QString::number(font.weight() * 8); QStringList decorations; - if (font.strikeOut()) { decorations.append("line-through"); } - if (font.underline()) { decorations.append("underline"); } + if (font.strikeOut()) { + decorations.append("line-through"); + } + if (font.underline()) { + decorations.append("underline"); + } return "font-family:'" + font.family() + "'; font-size:" + size + "; font-style:" + style + "; font-weight:" + weight + "; text-decoration:" + (decorations.isEmpty() ? "none" : decorations.join(" ")) + ";"; } @@ -800,16 +811,19 @@ QFont qFontFromString(const QString &str) { QFont font; font.fromString(str); - if (font.family().isEmpty()) - { font.setFamily(font.defaultFamily()); } + if (font.family().isEmpty()) { + font.setFamily(font.defaultFamily()); + } return font; } bool isFileParentWithSuffix(const QString &fileName, const QString &parent, const QStringList &suffixes) { - for (const QString &suffix : suffixes) - if (fileName == parent + suffix) + for (const QString &suffix : suffixes) { + if (fileName == parent + suffix) { return true; + } + } return false; } QList> listFilesFromDirectory(const QDir &dir, const QStringList &suffixes) @@ -817,21 +831,19 @@ QList> listFilesFromDirectory(const QDir &dir, const auto files = QList>(); QDirIterator it(dir, QDirIterator::Subdirectories); - while (it.hasNext()) - { + while (it.hasNext()) { it.next(); - if (it.fileInfo().isDir()) + if (it.fileInfo().isDir()) { continue; + } QString path = it.filePath(); const QString fileName = path.right(path.length() - dir.absolutePath().length() - 1); - if (!files.isEmpty()) - { + if (!files.isEmpty()) { const QString &previous = files.last().first; - if (isFileParentWithSuffix(fileName, previous, suffixes)) - { + if (isFileParentWithSuffix(fileName, previous, suffixes)) { files.last().second.append(fileName); continue; } @@ -843,6 +855,23 @@ QList> listFilesFromDirectory(const QDir &dir, const return files; } +QUrl removeCacheBuster(QUrl url) +{ + const QString query = url.query(); + if (query.isEmpty()) { + return url; + } + + // Only remove ?integer + bool ok; + query.toInt(&ok); + if (ok) { + url.setQuery(QString()); + } + + return url; +} + bool isVariantEmpty(const QVariant &value) { switch (value.type()) @@ -864,3 +893,15 @@ QString decodeHtmlEntities(const QString &html) decode_html_entities_utf8(dest, src); return QString::fromUtf8(dest); } + +QStringList jsToStringList(const QJSValue &val) +{ + QStringList ret; + + const quint32 length = val.property("length").toUInt(); + for (quint32 i = 0; i < length; ++i) { + ret.append(val.property(i).toString()); + } + + return ret; +} diff --git a/lib/src/functions.h b/lib/src/functions.h index f0df8a9f4..7c89fbed6 100644 --- a/lib/src/functions.h +++ b/lib/src/functions.h @@ -4,24 +4,17 @@ #include #include #include +#include #include #include #include -#include "logger.h" +#include "backports/backports.h" +#include "filename/conditional-filename.h" class QSettings; -// qAsConst -#if (QT_VERSION < QT_VERSION_CHECK(5, 7, 0)) - template - Q_DECL_CONSTEXPR typename std::add_const::type &qAsConst(T &t) Q_DECL_NOTHROW { return t; } - - template - void qAsConst(const T &&) Q_DECL_EQ_DELETE; -#endif - // Filesize units #if defined(Q_OS_WIN) // 1 KB = 1024 B @@ -58,10 +51,8 @@ QString fixFilename(QString filename, QString path = "", int maxLength = 0, bool QString fixFilenameWindows(const QString &fn, const QString &path = "", int maxLength = 0, bool invalidChars = true); QString fixFilenameLinux(const QString &fn, const QString &path = "", int maxLength = 0, bool invalidChars = true); -QMap domToMap(const QDomElement &); - QMap getCustoms(QSettings *settings); -QMap> getFilenames(QSettings *settings); +QList getFilenames(QSettings *settings); QMap> getExternalLogFiles(QSettings *settings); QStringList getExternalLogFilesSuffixes(QSettings *settings); @@ -85,6 +76,9 @@ QFont qFontFromString(const QString &str); QList> listFilesFromDirectory(const QDir &dir, const QStringList &suffixes); +QUrl removeCacheBuster(QUrl url); +QStringList jsToStringList(const QJSValue &val); + template diff --git a/lib/src/language-loader.cpp b/lib/src/language-loader.cpp index 605df6fc4..a29918890 100644 --- a/lib/src/language-loader.cpp +++ b/lib/src/language-loader.cpp @@ -17,39 +17,40 @@ QMap LanguageLoader::getAllLanguages() const QStringList languageFiles = QDir(m_path).entryList(QStringList() << QStringLiteral("*.qm"), QDir::Files); QMap languages; - for (const QString &languageFile : languageFiles) - { + for (const QString &languageFile : languageFiles) { const QString lang = languageFile.left(languageFile.length() - 3); const QString fullLang = fullLanguages.value(lang, lang).toString(); languages[lang] = fullLang; } - if (!languages.contains("English")) - { + if (!languages.contains("English")) { languages[""] = "English"; } return languages; } -void LanguageLoader::install(QCoreApplication *app) +bool LanguageLoader::install(QCoreApplication *app) { - app->installTranslator(&m_translator); - app->installTranslator(&m_qtTranslator); + const bool general = app->installTranslator(&m_translator); + const bool qt = app->installTranslator(&m_qtTranslator); + return general && qt; } -void LanguageLoader::uninstall(QCoreApplication *app) +bool LanguageLoader::uninstall(QCoreApplication *app) { - app->removeTranslator(&m_translator); - app->removeTranslator(&m_qtTranslator); + const bool general = app->removeTranslator(&m_translator); + const bool qt = app->removeTranslator(&m_qtTranslator); + return general && qt; } -void LanguageLoader::setLanguage(const QString &lang) +bool LanguageLoader::setLanguage(const QString &lang) { log(QStringLiteral("Setting language to '%1'...").arg(lang), Logger::Info); QLocale::setDefault(QLocale(lang)); - m_translator.load(m_path + lang + ".qm"); - m_qtTranslator.load(m_path + "qt/" + lang + ".qm"); + const bool general = m_translator.load(m_path + lang + ".qm"); + const bool qt = m_qtTranslator.load(m_path + "qt/" + lang + ".qm"); + return general && qt; } diff --git a/lib/src/language-loader.h b/lib/src/language-loader.h index cd427ee1a..c130df864 100644 --- a/lib/src/language-loader.h +++ b/lib/src/language-loader.h @@ -16,11 +16,11 @@ class LanguageLoader : public QObject public: explicit LanguageLoader(QString path); QMap getAllLanguages() const; - void install(QCoreApplication *app); - void uninstall(QCoreApplication *app); + bool install(QCoreApplication *app); + bool uninstall(QCoreApplication *app); public slots: - void setLanguage(const QString &lang); + bool setLanguage(const QString &lang); private: QString m_path; diff --git a/lib/src/loader/downloadable-downloader.cpp b/lib/src/loader/downloadable-downloader.cpp deleted file mode 100644 index 62f670c13..000000000 --- a/lib/src/loader/downloadable-downloader.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "loader/downloadable-downloader.h" -#include "functions.h" -#include "logger.h" -#include "models/site.h" - - -DownloadableDownloader::DownloadableDownloader(QSharedPointer downloadable, Site *site, int count, bool addMd5, bool startCommands, bool loadTags, QObject *parent) - : QObject(parent), m_downloadable(std::move(downloadable)), m_site(site), m_count(count), m_addMd5(addMd5), m_startCommands(startCommands), m_loadTags(loadTags), m_fileDownloader(false, this) -{} - -void DownloadableDownloader::setPath(const Filename &filename, const QString &folder) -{ - m_filename = filename; - m_folder = folder; -} - -void DownloadableDownloader::setPath(const QStringList &paths) -{ - m_paths = paths; -} - -void DownloadableDownloader::setResult(const QStringList &keys, Downloadable::SaveResult value) -{ - for (const QString &key : keys) - m_result.insert(key, value); -} - -void DownloadableDownloader::save() -{ - m_downloadable->preload(m_filename); - preloaded(); -} - -void DownloadableDownloader::preloaded() -{ - const QUrl url = m_downloadable->url(Downloadable::Size::Full); - QStringList paths = !m_paths.isEmpty() ? m_paths : m_downloadable->paths(m_filename, m_folder, m_count); - - // Sometimes we don't even need to download the image to save it - m_paths.clear(); - m_result.clear(); - for (const QString &path : paths) - { - Downloadable::SaveResult result = m_downloadable->preSave(path); - if (result == Downloadable::SaveResult::NotLoaded) - { m_paths.append(path); } - else - { m_result.insert(path, result); }; - } - - // If we don't need any loading, we return early - if (m_paths.isEmpty()) - { - for (auto it = m_result.constBegin(); it != m_result.constEnd(); ++it) - { m_downloadable->postSave(it.key(), it.value(), m_addMd5, m_startCommands, m_count); } - emit saved(m_downloadable, m_result); - return; - } - - // Load the image directly on the disk - log(QStringLiteral("Loading and saving image in `%1`").arg(m_paths.first())); - m_url = m_site->fixUrl(url.toString()); - QNetworkReply *reply = m_site->get(m_url, nullptr, QStringLiteral("image"), nullptr); // TODO(Bionus) - connect(&m_fileDownloader, &FileDownloader::writeError, this, &DownloadableDownloader::writeError, Qt::UniqueConnection); - connect(&m_fileDownloader, &FileDownloader::networkError, this, &DownloadableDownloader::networkError, Qt::UniqueConnection); - connect(&m_fileDownloader, &FileDownloader::success, this, &DownloadableDownloader::success, Qt::UniqueConnection); - - // If we can't start writing for some reason, return an error - if (!m_fileDownloader.start(reply, m_paths)) - { - log(QStringLiteral("Unable to open file"), Logger::Error); - setResult(m_paths, Downloadable::SaveResult::Error); - emit saved(m_downloadable, m_result); - } -} - -void DownloadableDownloader::writeError() -{ - setResult(m_paths, Downloadable::SaveResult::Error); - emit saved(m_downloadable, m_result); -} - -void DownloadableDownloader::networkError(QNetworkReply::NetworkError error, const QString &errorString) -{ - // Ignore cancel errors - if (error == QNetworkReply::OperationCanceledError) - return; - - if (error == QNetworkReply::ContentNotFoundError) - { - setResult(m_paths, Downloadable::SaveResult::NotFound); - } - else - { - log(QStringLiteral("Network error for the image: `%1`: %2 (%3)").arg(m_url.toString().toHtmlEscaped()).arg(error).arg(errorString), Logger::Error); - setResult(m_paths, Downloadable::SaveResult::NetworkError); - } - - emit saved(m_downloadable, m_result); -} - -void DownloadableDownloader::success() -{ - setResult(m_paths, Downloadable::SaveResult::Saved); - for (const QString &path : qAsConst(m_paths)) - { m_downloadable->postSave(path, Downloadable::SaveResult::Saved, m_addMd5, m_startCommands, m_count); } - emit saved(m_downloadable, m_result); -} diff --git a/lib/src/loader/downloadable-downloader.h b/lib/src/loader/downloadable-downloader.h deleted file mode 100644 index fcbf6a6c2..000000000 --- a/lib/src/loader/downloadable-downloader.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef DOWNLOADABLE_DOWNLOADER_H -#define DOWNLOADABLE_DOWNLOADER_H - -#include -#include -#include -#include -#include -#include -#include -#include "downloader/file-downloader.h" -#include "loader/downloadable.h" -#include "models/filename.h" - - -class Site; - -class DownloadableDownloader : public QObject -{ - Q_OBJECT - - public: - explicit DownloadableDownloader(QSharedPointer downloadable, Site *site, int count, bool addMd5, bool startCommands, bool loadTags, QObject *parent = nullptr); - void setPath(const Filename &filename, const QString &folder); - void setPath(const QStringList &paths); - void save(); - - protected slots: - void preloaded(); - void writeError(); - void networkError(QNetworkReply::NetworkError error, const QString &errorString); - void success(); - - signals: - void saved(QSharedPointer downloadable, const QMap &result); - - protected: - void setResult(const QStringList &keys, Downloadable::SaveResult value); - - private: - QSharedPointer m_downloadable; - Filename m_filename; - QString m_folder; - Site *m_site; - int m_count; - bool m_addMd5; - bool m_startCommands; - bool m_loadTags; - QUrl m_url; - QStringList m_paths; - FileDownloader m_fileDownloader; - QMap m_result; -}; - -#endif // DOWNLOADABLE_DOWNLOADER_H diff --git a/lib/src/loader/downloadable.cpp b/lib/src/loader/downloadable.cpp index 5dba24650..89d0c21ee 100644 --- a/lib/src/loader/downloadable.cpp +++ b/lib/src/loader/downloadable.cpp @@ -20,29 +20,28 @@ bool isFavorited(const QStringList &tags, const QList &favorites) const QMap &Downloadable::tokens(Profile *profile) const { - if (m_tokens.isEmpty()) - { + if (m_tokens.isEmpty()) { auto tokens = generateTokens(profile); // Custom tokens (if the tokens contain tags) - if (tokens.contains("tags")) - { + if (tokens.contains("tags")) { const QStringList &tags = tokens["tags"].value().toStringList(); QMap scustom = getCustoms(profile->getSettings()); QMap custom; - for (const QString &tag : tags) - { - for (auto it = scustom.constBegin(); it != scustom.constEnd(); ++it) - { + for (const QString &tag : tags) { + for (auto it = scustom.constBegin(); it != scustom.constEnd(); ++it) { const QString &key = it.key(); - if (!custom.contains(key)) - { custom.insert(key, QStringList()); } - if (it.value().contains(tag, Qt::CaseInsensitive)) - { custom[key].append(tag); } + if (!custom.contains(key)) { + custom.insert(key, QStringList()); + } + if (it.value().contains(tag, Qt::CaseInsensitive)) { + custom[key].append(tag); + } } } - for (auto it = custom.constBegin(); it != custom.constEnd(); ++it) - { tokens.insert(it.key(), Token(it.value())); } + for (auto it = custom.constBegin(); it != custom.constEnd(); ++it) { + tokens.insert(it.key(), Token(it.value())); + } } // Use a lazy token for Grabber meta-tags as it can be expensive to calculate @@ -52,10 +51,8 @@ const QMap &Downloadable::tokens(Profile *profile) const Filename filename(profile->getSettings()->value("Save/filename").toString()); QStringList paths = filename.path(tokens, profile, pth); bool alreadyExists = false; - for (const QString &path : paths) - { - if (QFile::exists(path)) - { + for (const QString &path : paths) { + if (QFile::exists(path)) { alreadyExists = true; break; } @@ -64,12 +61,15 @@ const QMap &Downloadable::tokens(Profile *profile) const // Generate corresponding combination QStringList metas; - if (alreadyExists) - { metas.append("alreadyExists"); } - if (inMd5List) - { metas.append("inMd5List"); } - if (alreadyExists || inMd5List) - { metas.append("downloaded"); } + if (alreadyExists) { + metas.append("alreadyExists"); + } + if (inMd5List) { + metas.append("inMd5List"); + } + if (alreadyExists || inMd5List) { + metas.append("downloaded"); + } // Favorited if (tokens.contains("tags")) { diff --git a/lib/src/loader/downloadable.h b/lib/src/loader/downloadable.h index 1ccb7e87d..a9d15803e 100644 --- a/lib/src/loader/downloadable.h +++ b/lib/src/loader/downloadable.h @@ -36,6 +36,7 @@ class Downloadable enum Size { + Unknown, Thumbnail, Sample, Full @@ -46,8 +47,8 @@ class Downloadable virtual QUrl url(Size size) const = 0; virtual QStringList paths(const Filename &filename, const QString &folder, int count) const = 0; const QMap &tokens(Profile *profile) const; - virtual SaveResult preSave(const QString &path) = 0; - virtual void postSave(const QString &path, SaveResult result, bool addMd5, bool startCommands, int count) = 0; + virtual SaveResult preSave(const QString &path, Size size) = 0; + virtual void postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count) = 0; virtual QColor color() const = 0; virtual QString tooltip() const = 0; diff --git a/lib/src/loader/loader-query.cpp b/lib/src/loader/loader-query.cpp index 711ebf346..24ba7fc24 100644 --- a/lib/src/loader/loader-query.cpp +++ b/lib/src/loader/loader-query.cpp @@ -28,8 +28,9 @@ LoaderData LoaderQuery::next() LoaderData ret; // Early return if we try to load the next results of a finished query - if (m_finished) + if (m_finished) { return ret; + } // Options Profile *profile = m_site->getSource()->getProfile(); @@ -52,11 +53,9 @@ LoaderData LoaderQuery::next() // Add results to the data object const QList> &images = request.images(); - for (const QSharedPointer &img : images) - { + for (const QSharedPointer &img : images) { // Skip blacklisted images - if (!getBlacklisted && !blacklist.match(img->tokens(profile)).empty()) - { + if (!getBlacklisted && !blacklist.match(img->tokens(profile)).empty()) { ret.ignored.append(img); continue; } diff --git a/lib/src/loader/pack-loader.cpp b/lib/src/loader/pack-loader.cpp index ab7b7a118..7be7badd2 100644 --- a/lib/src/loader/pack-loader.cpp +++ b/lib/src/loader/pack-loader.cpp @@ -22,15 +22,28 @@ bool PackLoader::start() m_site->login(); loop.exec(); + // Resume stopped downloads + int page = m_query.page; + if (m_query.progressVal > 0) { + const int pagesToSkip = qFloor(m_query.progressVal / m_query.perpage); + page += pagesToSkip; + m_total = pagesToSkip * m_query.perpage; + } + // Add the first results page - m_pendingPages.append(new Page(m_profile, m_site, QList() << m_site, m_query.tags.split(' '), m_query.page, m_query.perpage, m_query.postFiltering, false, nullptr)); + m_pendingPages.append(new Page(m_profile, m_site, QList() << m_site, m_query.query, page, m_query.perpage, m_query.postFiltering, false, nullptr)); return true; } +void PackLoader::abort() +{ + m_abort = true; +} + bool PackLoader::hasNext() const { - return (!m_pendingPages.isEmpty() || !m_pendingGalleries.isEmpty()) && (m_total < m_query.total || m_query.total < 0); + return (!m_overflow.isEmpty() || !m_pendingPages.isEmpty() || !m_pendingGalleries.isEmpty()) && (m_total < m_query.total || m_query.total < 0); } QList> PackLoader::next() @@ -39,11 +52,29 @@ QList> PackLoader::next() const int already = m_total; QList> results; - int count = 0; int pageCount = 0; - while (hasNext() && pageCount < maxPages && (count == 0 || count < m_packSize || m_packSize < 0)) - { + if (!m_overflow.isEmpty()) { + while (!m_overflow.isEmpty() && (results.isEmpty() || results.count() < m_packSize || m_packSize < 0) && (already + results.count() != m_query.total || (m_overflowGallery && m_query.galleriesCountAsOne))) { + results.append(m_overflow.takeFirst()); + } + + if (!m_overflowGallery || !m_query.galleriesCountAsOne) { + m_total += results.count(); + } + + // If the overflow was the end of a gallery and we finished it, increase the counter + if (m_overflowGallery && m_overflow.isEmpty() && m_query.galleriesCountAsOne && !m_overflowHasNext) { + m_total++; + } + } + + while (hasNext() && pageCount < maxPages && (results.isEmpty() || results.count() < m_packSize || m_packSize < 0)) { + if (m_abort) { + m_abort = false; + break; + } + bool gallery = !m_pendingGalleries.isEmpty(); // Load next page/gallery @@ -56,41 +87,56 @@ QList> PackLoader::next() emit finishedPage(page); // Add next page to the pending queue - if (!gallery || page->hasNext()) - { - Page *next = new Page(m_profile, m_site, QList() << m_site, page->search(), page->page() + 1, m_query.perpage, m_query.postFiltering, false, nullptr); - if (gallery) + if (page->hasNext()) { + Page *next = new Page(m_profile, m_site, QList() << m_site, page->query(), page->page() + 1, m_query.perpage, m_query.postFiltering, false, nullptr); + next->setLastPage(page); + if (gallery) { m_pendingGalleries.prepend(next); - else + } else { m_pendingPages.append(next); + } } // Add results to the data object auto itGallery = m_pendingGalleries.begin(); - for (const QSharedPointer &img : page->images()) - { + for (const QSharedPointer &img : page->images()) { // If this result is a gallery, add it to the beginning of the pending galleries - if (img->isGallery()) - { - Page *galleryPage = new Page(m_profile, m_site, QList() << m_site, QStringList() << ("gallery:" + img->md5()), 1, m_query.perpage, m_query.postFiltering, false, nullptr); - // gallery->addToken("gallery_name", img->name()); + if (img->isGallery()) { + SearchQuery q; + q.gallery = img; + q.tags = page->search(); + Page *galleryPage = new Page(m_profile, m_site, QList() << m_site, q, 1, m_query.perpage, m_query.postFiltering, false, nullptr); m_pendingGalleries.insert(itGallery, galleryPage); continue; } // If it's an image, add it to the results - results.append(img); + if (results.count() >= m_packSize) { + m_overflow.append(img); + m_overflowGallery = gallery; + m_overflowHasNext = page->hasNext(); + } else { + results.append(img); + + if (!gallery || !m_query.galleriesCountAsOne) { + m_total++; + } + } // Early return if we reached the image limit - if (already + results.count() == m_query.total) + if (m_total == m_query.total) { break; + } } - m_total += page->pageImageCount(); - count += page->pageImageCount(); + // If it's the last page of a gallery, increase the counter if we treated all images + if (gallery && !page->hasNext() && m_query.galleriesCountAsOne && m_overflow.isEmpty()) { + m_total++; + } - if (!gallery) + if (!gallery) { pageCount++; + } page->deleteLater(); } diff --git a/lib/src/loader/pack-loader.h b/lib/src/loader/pack-loader.h index d7cfccc50..f15099217 100644 --- a/lib/src/loader/pack-loader.h +++ b/lib/src/loader/pack-loader.h @@ -1,8 +1,8 @@ #ifndef PACK_LOADER_H #define PACK_LOADER_H -#include #include +#include #include #include #include "downloader/download-query-group.h" @@ -22,6 +22,7 @@ class PackLoader : public QObject const DownloadQueryGroup &query() const; int nextPackSize() const; bool start(); + void abort(); bool hasNext() const; QList> next(); @@ -36,6 +37,10 @@ class PackLoader : public QObject int m_total = 0; QLinkedList m_pendingPages; QLinkedList m_pendingGalleries; + QList> m_overflow; + bool m_overflowGallery = false; + bool m_overflowHasNext = false; + bool m_abort = false; }; #endif // PACK_LOADER_H diff --git a/lib/src/loader/token.cpp b/lib/src/loader/token.cpp index a9bb7445d..31e06e871 100644 --- a/lib/src/loader/token.cpp +++ b/lib/src/loader/token.cpp @@ -17,12 +17,14 @@ Token::Token(std::function func, bool cacheResult) QVariant Token::value() const { - if (m_func == nullptr || m_value.isValid()) - { return m_value; } + if (m_func == nullptr || m_value.isValid()) { + return m_value; + } QVariant val = m_func(); - if (m_cacheResult) - { m_value = val; } + if (m_cacheResult) { + m_value = val; + } return val; } diff --git a/lib/src/loader/token.h b/lib/src/loader/token.h index 0b5992ac5..697dd79ea 100644 --- a/lib/src/loader/token.h +++ b/lib/src/loader/token.h @@ -34,4 +34,6 @@ class Token bool operator==(const Token &lhs, const Token &rhs); bool operator!=(const Token &lhs, const Token &rhs); +Q_DECLARE_METATYPE(Token) + #endif // TOKEN_H diff --git a/lib/src/logger.cpp b/lib/src/logger.cpp index 2f8d7f99e..38218777c 100644 --- a/lib/src/logger.cpp +++ b/lib/src/logger.cpp @@ -6,11 +6,20 @@ #include #endif +void Logger::logToConsole() +{ + if (m_logFile.isOpen()) { + m_logFile.close(); + } + m_logFile.open(stdout, QIODevice::WriteOnly); +} + void Logger::setLogFile(const QString &path) { - if (m_logFile.isOpen()) - { m_logFile.close(); } + if (m_logFile.isOpen()) { + m_logFile.close(); + } m_logFile.setFileName(path); m_logFile.open(QFile::Append | QFile::Text | QFile::Truncate); @@ -23,6 +32,11 @@ void Logger::setLogLevel(LogLevel level) m_level = level; } +void Logger::setExitOnError(bool val) +{ + m_exitOnError = val; +} + void Logger::messageOutput(QtMsgType type, const QMessageLogContext &context, const QString &message) { @@ -40,8 +54,9 @@ void Logger::messageOutput(QtMsgType type, const QMessageLogContext &context, co QString label = QStringLiteral("[Qt]"); QString category(context.category); - if (!category.isEmpty()) + if (!category.isEmpty()) { label += "[" + category + "]"; + } #if defined QT_MESSAGELOGCONTEXT && defined QT_DEBUG && 0 label += QStringLiteral("[%1(%2)::%3]").arg(context.file).arg(context.line).arg(context.function); #endif @@ -67,11 +82,17 @@ void Logger::setupMessageOutput(bool log) */ void Logger::log(const QString &l, LogLevel level) { - if (level < m_level) + if (m_exitOnError && level == Logger::LogLevel::Error) { + throw std::runtime_error(l.toStdString()); + } + + if (level < m_level) { return; + } - if (!m_logFile.isOpen()) + if (!m_logFile.isOpen()) { setLogFile(savePath(QStringLiteral("main.log"), false, true)); + } static const QString timeFormat = QStringLiteral("hh:mm:ss.zzz"); static const QStringList levels = QStringList() @@ -97,8 +118,7 @@ void Logger::log(const QString &l, LogLevel level) void Logger::logCommand(const QString &l) { - if (!m_fCommandsLog.isOpen()) - { + if (!m_fCommandsLog.isOpen()) { m_fCommandsLog.setFileName(savePath(QStringLiteral("commands.log"), false, true)); m_fCommandsLog.open(QFile::Append | QFile::Text | QFile::Truncate); } @@ -109,8 +129,7 @@ void Logger::logCommand(const QString &l) void Logger::logCommandSql(const QString &l) { - if (!m_fCommandsSqlLog.isOpen()) - { + if (!m_fCommandsSqlLog.isOpen()) { m_fCommandsSqlLog.setFileName(savePath(QStringLiteral("commands.sql"), false, true)); m_fCommandsSqlLog.open(QFile::Append | QFile::Text | QFile::Truncate); } diff --git a/lib/src/logger.h b/lib/src/logger.h index 745555e5b..531e8ee96 100644 --- a/lib/src/logger.h +++ b/lib/src/logger.h @@ -36,12 +36,14 @@ class Logger : public QObject static void noMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &message); static void setupMessageOutput(bool log); + void setExitOnError(bool val); void setLogFile(const QString &path); void setLogLevel(LogLevel level); void log(const QString &, LogLevel level = Info); void logCommand(const QString &); void logCommandSql(const QString &); void logUpdate(const QString &); + void logToConsole(); QString logFile() const; @@ -52,6 +54,7 @@ class Logger : public QObject Logger() = default; QFile m_logFile, m_fCommandsLog, m_fCommandsSqlLog; LogLevel m_level = LogLevel::Info; + bool m_exitOnError = false; }; diff --git a/lib/src/login/http-get-login.cpp b/lib/src/login/http-get-login.cpp index 3cb3255b2..f8ca13c0c 100644 --- a/lib/src/login/http-get-login.cpp +++ b/lib/src/login/http-get-login.cpp @@ -4,8 +4,8 @@ #include "models/site.h" -HttpGetLogin::HttpGetLogin(Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) - : HttpLogin("get", site, manager, settings) +HttpGetLogin::HttpGetLogin(HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) + : HttpLogin("get", auth, site, manager, settings) {} QNetworkReply *HttpGetLogin::getReply(const QString &url, const QUrlQuery &query) const diff --git a/lib/src/login/http-get-login.h b/lib/src/login/http-get-login.h index 50deb69a3..de3da4a75 100644 --- a/lib/src/login/http-get-login.h +++ b/lib/src/login/http-get-login.h @@ -5,6 +5,7 @@ class CustomNetworkAccessManager; +class HttpAuth; class MixedSettings; class QNetworkReply; class QUrlQuery; @@ -15,7 +16,7 @@ class HttpGetLogin : public HttpLogin Q_OBJECT public: - explicit HttpGetLogin(Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); + explicit HttpGetLogin(HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); QNetworkReply *getReply(const QString &url, const QUrlQuery &query) const override; }; diff --git a/lib/src/login/http-login.cpp b/lib/src/login/http-login.cpp index 680931b4f..4c6161c92 100644 --- a/lib/src/login/http-login.cpp +++ b/lib/src/login/http-login.cpp @@ -3,57 +3,55 @@ #include #include #include +#include "auth/auth-field.h" +#include "auth/http-auth.h" #include "custom-network-access-manager.h" #include "mixed-settings.h" +#include "models/site.h" -HttpLogin::HttpLogin(QString type, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) - : m_type(std::move(type)), m_site(site), m_loginReply(nullptr), m_manager(manager), m_settings(settings) +HttpLogin::HttpLogin(QString type, HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) + : m_type(std::move(type)), m_auth(auth), m_site(site), m_loginReply(nullptr), m_manager(manager), m_settings(settings) {} bool HttpLogin::isTestable() const { - return !m_settings->value("login/" + m_type + "/url").toString().isEmpty(); + return !m_auth->url().isEmpty(); } void HttpLogin::login() { - const QString username = m_settings->value("auth/pseudo").toString(); - const QString password = m_settings->value("auth/password").toString(); - QUrlQuery query; - query.addQueryItem(m_settings->value("login/" + m_type + "/pseudo").toString(), username); - query.addQueryItem(m_settings->value("login/" + m_type + "/password").toString(), password); - - m_settings->beginGroup("login/fields"); - QStringList keys = m_settings->childKeys(); - for (const QString &key : keys) - { query.addQueryItem(key, m_settings->value(key).toString()); } - m_settings->endGroup(); + for (AuthField *field : m_auth->fields()) { + if (!field->key().isEmpty()) { + query.addQueryItem(field->key(), field->value(m_settings)); + } + } - if (m_loginReply != nullptr) - { + if (m_loginReply != nullptr) { m_loginReply->abort(); m_loginReply->deleteLater(); } - const QString loginUrl = m_settings->value("login/" + m_type + "/url").toString(); - m_loginReply = getReply(loginUrl, query); + QNetworkReply *reply = getReply(m_auth->url(), query); + if (reply == nullptr) { + emit loggedIn(Result::Failure); + return; + } + m_loginReply = reply; connect(m_loginReply, &QNetworkReply::finished, this, &HttpLogin::loginFinished); } void HttpLogin::loginFinished() { - const QString cookieName = m_settings->value("login/" + m_type + "/cookie").toString(); + const QString cookieName = m_auth->cookie(); QNetworkCookieJar *cookieJar = m_manager->cookieJar(); QList cookies = cookieJar->cookiesForUrl(m_loginReply->url()); - for (const QNetworkCookie &cookie : cookies) - { - if (cookie.name() == cookieName && !cookie.value().isEmpty() && cookie.value() != "0") - { + for (const QNetworkCookie &cookie : cookies) { + if (cookie.name() == cookieName && !cookie.value().isEmpty() && cookie.value() != "0") { emit loggedIn(Result::Success); return; } diff --git a/lib/src/login/http-login.h b/lib/src/login/http-login.h index 96d1738ad..d6fa2b5a6 100644 --- a/lib/src/login/http-login.h +++ b/lib/src/login/http-login.h @@ -6,6 +6,7 @@ class CustomNetworkAccessManager; +class HttpAuth; class MixedSettings; class QNetworkReply; class QUrlQuery; @@ -15,8 +16,10 @@ class HttpLogin : public Login { Q_OBJECT + protected: + HttpLogin(QString type, HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); + public: - explicit HttpLogin(QString type, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); virtual ~HttpLogin() = default; bool isTestable() const override; virtual QNetworkReply *getReply(const QString &url, const QUrlQuery &query) const = 0; @@ -29,6 +32,7 @@ class HttpLogin : public Login protected: QString m_type; + HttpAuth *m_auth; Site *m_site; QNetworkReply *m_loginReply; CustomNetworkAccessManager *m_manager; diff --git a/lib/src/login/http-post-login.cpp b/lib/src/login/http-post-login.cpp index 836468ca8..a58271225 100644 --- a/lib/src/login/http-post-login.cpp +++ b/lib/src/login/http-post-login.cpp @@ -4,8 +4,8 @@ #include "models/site.h" -HttpPostLogin::HttpPostLogin(Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) - : HttpLogin("post", site, manager, settings) +HttpPostLogin::HttpPostLogin(HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) + : HttpLogin("post", auth, site, manager, settings) {} QNetworkReply *HttpPostLogin::getReply(const QString &url, const QUrlQuery &query) const diff --git a/lib/src/login/http-post-login.h b/lib/src/login/http-post-login.h index 11b746894..ecbbe05b4 100644 --- a/lib/src/login/http-post-login.h +++ b/lib/src/login/http-post-login.h @@ -5,6 +5,7 @@ class CustomNetworkAccessManager; +class HttpAuth; class MixedSettings; class QNetworkReply; class QUrlQuery; @@ -15,7 +16,7 @@ class HttpPostLogin : public HttpLogin Q_OBJECT public: - explicit HttpPostLogin(Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); + explicit HttpPostLogin(HttpAuth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); QNetworkReply *getReply(const QString &url, const QUrlQuery &query) const override; }; diff --git a/lib/src/login/login.cpp b/lib/src/login/login.cpp index ea1cc1fa1..9fe557e31 100644 --- a/lib/src/login/login.cpp +++ b/lib/src/login/login.cpp @@ -1,10 +1,8 @@ #include "login/login.h" -QString Login::complementUrl(QString url, const QString &loginPart) const +QString Login::complementUrl(QString url) const { - Q_UNUSED(loginPart); - return url; } diff --git a/lib/src/login/login.h b/lib/src/login/login.h index a111f9df6..1649ec6c6 100644 --- a/lib/src/login/login.h +++ b/lib/src/login/login.h @@ -19,7 +19,7 @@ class Login : public QObject }; virtual bool isTestable() const = 0; - virtual QString complementUrl(QString url, const QString &loginPart) const; + virtual QString complementUrl(QString url) const; virtual void complementRequest(QNetworkRequest *request) const; public slots: diff --git a/lib/src/login/oauth2-login.cpp b/lib/src/login/oauth2-login.cpp index 277b9660d..077d36d2c 100644 --- a/lib/src/login/oauth2-login.cpp +++ b/lib/src/login/oauth2-login.cpp @@ -1,51 +1,47 @@ #include "login/oauth2-login.h" #include #include -#include #include +#include "auth/oauth2-auth.h" +#include "custom-network-access-manager.h" #include "logger.h" #include "mixed-settings.h" #include "models/site.h" -typedef QPair QStrP; +using QStrP = QPair; -OAuth2Login::OAuth2Login(Site *site, QNetworkAccessManager *manager, MixedSettings *settings) - : m_site(site), m_manager(manager), m_settings(settings), m_tokenReply(nullptr) +OAuth2Login::OAuth2Login(OAuth2Auth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings) + : m_auth(auth), m_site(site), m_manager(manager), m_settings(settings), m_tokenReply(nullptr) {} bool OAuth2Login::isTestable() const { - return !m_settings->value("login/oauth2/tokenUrl").toString().isEmpty(); + return !m_auth->tokenUrl().isEmpty(); } void OAuth2Login::login() { - const QString type = m_settings->value("login/oauth2/type", "password").toString(); + const QString type = m_auth->authType(); const QString consumerKey = m_settings->value("auth/consumerKey").toString(); const QString consumerSecret = m_settings->value("auth/consumerSecret").toString(); - QNetworkRequest request(m_site->fixUrl(m_settings->value("login/oauth2/tokenUrl").toString())); + QNetworkRequest request(m_site->fixUrl(m_auth->tokenUrl())); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded;charset=UTF-8"); QList body; - if (type == "header_basic") - { + if (type == "header_basic") { body << QStrP("grant_type", "client_credentials"); const QByteArray bearerCredentials = QUrl::toPercentEncoding(consumerKey) + ":" + QUrl::toPercentEncoding(consumerSecret); const QByteArray base64BearerCredentials = bearerCredentials.toBase64(); request.setRawHeader("Authorization", "Basic " + base64BearerCredentials); - } - else if (type == "client_credentials") - { + } else if (type == "client_credentials") { body << QStrP("grant_type", "client_credentials") << QStrP("client_id", consumerKey) << QStrP("client_secret", consumerSecret); - } - else if (type == "password") - { + } else if (type == "password") { const QString pseudo = m_settings->value("auth/pseudo").toString(); const QString password = m_settings->value("auth/password").toString(); @@ -53,18 +49,19 @@ void OAuth2Login::login() << QStrP("username", pseudo) << QStrP("password", password); - if (!consumerKey.isEmpty()) - { + if (!consumerKey.isEmpty()) { body << QStrP("client_id", consumerKey); - if (!consumerSecret.isEmpty()) - { body << QStrP("client_secret", consumerSecret); } + if (!consumerSecret.isEmpty()) { + body << QStrP("client_secret", consumerSecret); + } } } // Post request and wait for a reply QString bodyStr; - for (const QStrP &pair : body) - { bodyStr += (!bodyStr.isEmpty() ? "&" : "") + pair.first + "=" + pair.second; } + for (const QStrP &pair : body) { + bodyStr += (!bodyStr.isEmpty() ? "&" : "") + pair.first + "=" + pair.second; + } m_tokenReply = m_manager->post(request, bodyStr.toUtf8()); connect(m_tokenReply, &QNetworkReply::finished, this, &OAuth2Login::loginFinished); } @@ -76,18 +73,17 @@ void OAuth2Login::loginFinished() // Some OAuth2 API wrap their responses in 'response' JSON objects QJsonObject jsonObject = jsonDocument.object(); - if (!jsonObject.contains("token_type") && jsonObject.contains("response")) - { jsonObject = jsonObject.value("response").toObject(); } + if (!jsonObject.contains("token_type") && jsonObject.contains("response")) { + jsonObject = jsonObject.value("response").toObject(); + } const QJsonValue tokenType = jsonObject.value("token_type"); - if (tokenType.isUndefined()) - { + if (tokenType.isUndefined()) { log(QStringLiteral("[%1] No OAuth2 token type received: %2").arg(m_site->url(), result), Logger::Warning); emit loggedIn(Result::Failure); return; } - if (tokenType.toString() != QLatin1String("bearer")) - { + if (tokenType.toString() != QLatin1String("bearer")) { log(QStringLiteral("[%1] Wrong OAuth2 token type received (%2).").arg(m_site->url(), tokenType.toString()), Logger::Warning); emit loggedIn(Result::Failure); return; @@ -100,6 +96,7 @@ void OAuth2Login::loginFinished() void OAuth2Login::complementRequest(QNetworkRequest *request) const { - if (!m_token.isEmpty()) + if (!m_token.isEmpty()) { request->setRawHeader("Authorization", "Bearer " + m_token.toUtf8()); + } } diff --git a/lib/src/login/oauth2-login.h b/lib/src/login/oauth2-login.h index 8f6070553..7ce8644f4 100644 --- a/lib/src/login/oauth2-login.h +++ b/lib/src/login/oauth2-login.h @@ -5,8 +5,9 @@ #include "login/login.h" +class CustomNetworkAccessManager; class MixedSettings; -class QNetworkAccessManager; +class OAuth2Auth; class QNetworkReply; class QNetworkRequest; class Site; @@ -16,7 +17,7 @@ class OAuth2Login : public Login Q_OBJECT public: - explicit OAuth2Login(Site *site, QNetworkAccessManager *manager, MixedSettings *settings); + explicit OAuth2Login(OAuth2Auth *auth, Site *site, CustomNetworkAccessManager *manager, MixedSettings *settings); bool isTestable() const override; void complementRequest(QNetworkRequest *request) const override; @@ -27,8 +28,9 @@ class OAuth2Login : public Login void loginFinished(); private: + OAuth2Auth *m_auth; Site *m_site; - QNetworkAccessManager *m_manager; + CustomNetworkAccessManager *m_manager; MixedSettings *m_settings; QNetworkReply *m_tokenReply; QString m_token; diff --git a/lib/src/login/url-login.cpp b/lib/src/login/url-login.cpp index 445d5b137..80c1aa7f9 100644 --- a/lib/src/login/url-login.cpp +++ b/lib/src/login/url-login.cpp @@ -1,30 +1,33 @@ #include "login/url-login.h" -#include +#include "auth/auth-field.h" +#include "auth/url-auth.h" #include "mixed-settings.h" -#include "models/api/api.h" #include "models/page.h" #include "models/site.h" #include "models/source.h" -UrlLogin::UrlLogin(Site *site, QNetworkAccessManager *manager, MixedSettings *settings) - : m_site(site), m_manager(manager), m_settings(settings), m_page(nullptr) +UrlLogin::UrlLogin(UrlAuth *auth, Site *site, QNetworkAccessManager *manager, MixedSettings *settings) + : m_auth(auth), m_site(site), m_manager(manager), m_settings(settings), m_page(nullptr) {} bool UrlLogin::isTestable() const { - return m_settings->value("login/maxPage", 0).toInt() > 0; + return m_auth->maxPage() > 0; } void UrlLogin::login() { - if (m_page != nullptr) - { + if (m_page != nullptr) { + if (!m_page->isLoaded()) { + return; + } + m_page->abort(); m_page->deleteLater(); } - const int maxPageAnonymous = m_settings->value("login/maxPage", 0).toInt(); + const int maxPageAnonymous = m_auth->maxPage(); m_page = new Page(m_site->getSource()->getProfile(), m_site, QList() << m_site, QStringList(), maxPageAnonymous); connect(m_page, &Page::finishedLoading, this, &UrlLogin::loginFinished); connect(m_page, &Page::failedLoading, this, &UrlLogin::loginFinished); @@ -33,8 +36,7 @@ void UrlLogin::login() void UrlLogin::loginFinished() { - if (!m_page->images().isEmpty()) - { + if (!m_page->images().isEmpty()) { emit loggedIn(Result::Success); return; } @@ -42,25 +44,16 @@ void UrlLogin::loginFinished() emit loggedIn(Result::Failure); } -QString UrlLogin::complementUrl(QString url, const QString &loginPart) const +QString UrlLogin::complementUrl(QString url) const { - const QString pseudo = m_settings->value("auth/pseudo").toString(); - const QString password = m_settings->value("auth/password").toString(); - - const bool hasLoginString = !loginPart.isEmpty() && (!pseudo.isEmpty() || !password.isEmpty()); - url.replace("{login}", hasLoginString ? loginPart : QString()); - - // Basic GET auth - url.replace("{pseudo}", pseudo); - url.replace("{password}", password); - - // Appkey GET auth - if (url.contains("{appkey}")) - { - QString appkey = m_site->getApis().first()->value("AppkeySalt"); - appkey.replace("%password%", password); - appkey.replace("%username%", pseudo.toLower()); - url.replace("{appkey}", QCryptographicHash::hash(appkey.toUtf8(), QCryptographicHash::Sha1).toHex()); + bool hasQuery = url.contains('?'); + + int i = 0; + for (AuthField *field : m_auth->fields()) { + if (!field->key().isEmpty()) { + url.append((i == 0 && !hasQuery ? '?' : '&') + field->key() + "=" + field->value(m_settings)); + ++i; + } } return url; diff --git a/lib/src/login/url-login.h b/lib/src/login/url-login.h index 6f8a34771..3727da92d 100644 --- a/lib/src/login/url-login.h +++ b/lib/src/login/url-login.h @@ -9,15 +9,16 @@ class MixedSettings; class Page; class QNetworkAccessManager; class Site; +class UrlAuth; class UrlLogin : public Login { Q_OBJECT public: - explicit UrlLogin(Site *site, QNetworkAccessManager *manager, MixedSettings *settings); + explicit UrlLogin(UrlAuth *auth, Site *site, QNetworkAccessManager *manager, MixedSettings *settings); bool isTestable() const override; - QString complementUrl(QString url, const QString &loginPart) const override; + QString complementUrl(QString url) const override; public slots: void login() override; @@ -26,6 +27,7 @@ class UrlLogin : public Login void loginFinished(); private: + UrlAuth *m_auth; Site *m_site; QNetworkAccessManager *m_manager; MixedSettings *m_settings; diff --git a/lib/src/mixed-settings.cpp b/lib/src/mixed-settings.cpp index 62880a50b..77acd52cd 100644 --- a/lib/src/mixed-settings.cpp +++ b/lib/src/mixed-settings.cpp @@ -9,35 +9,34 @@ MixedSettings::MixedSettings(QList settings) MixedSettings::~MixedSettings() { - for (QSettings *setting : qAsConst(m_settings)) + for (QSettings *setting : qAsConst(m_settings)) { setting->deleteLater(); + } } QVariant MixedSettings::value(const QString &key, const QVariant &defaultValue) const { - for (QSettings *setting : qAsConst(m_settings)) - { + for (QSettings *setting : qAsConst(m_settings)) { QVariant val = setting->value(key); - if (val.isValid()) + if (val.isValid()) { return val; + } } return defaultValue; } void MixedSettings::setValue(const QString &key, const QVariant &value, const QVariant &defaultValue) { - if (m_settings.isEmpty()) + if (m_settings.isEmpty()) { return; + } // If the parent setting already have this value set - if (m_settings.count() > 1) - { + if (m_settings.count() > 1) { QVariant parent = m_settings[1]->value(key); - if (parent.isValid()) - { - if (parent == value) - { + if (parent.isValid()) { + if (parent == value) { m_settings[0]->remove(key); return; } @@ -49,8 +48,7 @@ void MixedSettings::setValue(const QString &key, const QVariant &value, const QV } // No valid parent value found - if (value == defaultValue) - { + if (value == defaultValue) { m_settings.first()->remove(key); return; } @@ -62,25 +60,29 @@ void MixedSettings::setValue(const QString &key, const QVariant &value, const QV QStringList MixedSettings::childKeys() const { QStringList keys; - for (QSettings *setting : qAsConst(m_settings)) + for (QSettings *setting : qAsConst(m_settings)) { keys.append(setting->childKeys()); + } return keys; } void MixedSettings::beginGroup(const QString &prefix) { - for (QSettings *setting : qAsConst(m_settings)) + for (QSettings *setting : qAsConst(m_settings)) { setting->beginGroup(prefix); + } } void MixedSettings::endGroup() { - for (QSettings *setting : qAsConst(m_settings)) + for (QSettings *setting : qAsConst(m_settings)) { setting->endGroup(); + } } void MixedSettings::sync() { - for (QSettings *setting : qAsConst(m_settings)) + for (QSettings *setting : qAsConst(m_settings)) { setting->sync(); + } } diff --git a/lib/src/models/api/api.cpp b/lib/src/models/api/api.cpp index d88f3189d..2f7526e3b 100644 --- a/lib/src/models/api/api.cpp +++ b/lib/src/models/api/api.cpp @@ -6,65 +6,61 @@ #include "models/source.h" -Api::Api(QString name, QMap data) - : m_name(std::move(name)), m_data(std::move(data)) -{ - QString prefix = "Urls/" + m_name; - for (auto it = m_data.begin(); it != m_data.end(); ++it) - { - if (it.key().startsWith(prefix)) - { - const QString k = it.key().right(it.key().length() - prefix.length() - 1); - m_data["Urls/" + k] = it.value(); - } - } -} +Api::Api(QString name) + : m_name(std::move(name)) +{} QString Api::getName() const { return m_name; } -bool Api::contains(const QString &key) const { return m_data.contains(key); } -QString Api::value(const QString &key) const { return m_data.value(key); } - -QSharedPointer Api::parseImage(Page *parentPage, QMap d, int position, const QList &tags) const +QSharedPointer Api::parseImage(Page *parentPage, QMap d, QVariantMap data, int position, const QList &tags) const { Site *site = parentPage->site(); + d["position"] = QString::number(position + 1); + // Remove dot before extension - if (d.contains("ext") && d["ext"][0] == '.') - { d["ext"] = d["ext"].mid(1); } + if (d.contains("ext") && d["ext"][0] == '.') { + d["ext"] = d["ext"].mid(1); + } // Set default values - if (!d.contains("file_url")) - { d["file_url"] = ""; } - if (!d.contains("sample_url")) - { d["sample_url"] = ""; } - if (!d.contains("preview_url")) - { d["preview_url"] = ""; } + if (!d.contains("file_url")) { + d["file_url"] = ""; + } + if (!d.contains("sample_url")) { + d["sample_url"] = ""; + } + if (!d.contains("preview_url")) { + d["preview_url"] = ""; + } - if (d["file_url"].isEmpty()) - { d["file_url"] = d["preview_url"]; } - if (d["sample_url"].isEmpty()) - { d["sample_url"] = d["preview_url"]; } + if (d["file_url"].isEmpty()) { + d["file_url"] = d["preview_url"]; + } + if (d["sample_url"].isEmpty()) { + d["sample_url"] = d["preview_url"]; + } QStringList errors; // If the file path is wrong (ends with "/.jpg") - if (errors.isEmpty() && d["file_url"].endsWith("/." + d["ext"])) - { errors.append(QStringLiteral("file url")); } + if (errors.isEmpty() && d["file_url"].endsWith("/." + d["ext"])) { + errors.append(QStringLiteral("file url")); + } - if (!errors.isEmpty()) - { + if (!errors.isEmpty()) { log(QStringLiteral("[%1][%2] Image #%3 ignored. Reason: %4.").arg(site->url(), m_name, QString::number(position + 1), errors.join(", ")), Logger::Info); return QSharedPointer(); } // Generate image - QSharedPointer img(new Image(site, d, site->getSource()->getProfile(), parentPage)); + QSharedPointer img(new Image(site, d, std::move(data), site->getSource()->getProfile(), parentPage)); img->moveToThread(this->thread()); - if (!tags.isEmpty()) + if (!tags.isEmpty()) { img->setTags(tags); + } return img; } diff --git a/lib/src/models/api/api.h b/lib/src/models/api/api.h index d41444982..745c0eddf 100644 --- a/lib/src/models/api/api.h +++ b/lib/src/models/api/api.h @@ -59,29 +59,30 @@ class Api : public QObject { Q_OBJECT - public: - Api(QString name, QMap data); + protected: + explicit Api(QString name); + public: // Getters QString getName() const; virtual bool needAuth() const = 0; - // Info getters - bool contains(const QString &key) const; - QString value(const QString &key) const; - QString operator[](const QString &key) const { return value(key); } - // API - virtual PageUrl pageUrl(const QString &search, int page, int limit, int lastPage, int lastPageMinId, int lastPageMaxId, Site *site) const = 0; - virtual ParsedPage parsePage(Page *parentPage, const QString &source, int first) const = 0; - virtual PageUrl galleryUrl(const QString &id, int page, int limit, Site *site) const = 0; - virtual ParsedPage parseGallery(Page *parentPage, const QString &source, int first) const = 0; + virtual PageUrl pageUrl(const QString &search, int page, int limit, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId, Site *site) const = 0; + virtual bool parsePageErrors() const = 0; + virtual ParsedPage parsePage(Page *parentPage, const QString &source, int statusCode, int first) const = 0; + virtual PageUrl galleryUrl(const QSharedPointer &gallery, int page, int limit, Site *site) const = 0; + virtual bool parseGalleryErrors() const = 0; + virtual ParsedPage parseGallery(Page *parentPage, const QString &source, int statusCode, int first) const = 0; virtual PageUrl tagsUrl(int page, int limit, Site *site) const = 0; - virtual ParsedTags parseTags(const QString &source, Site *site) const = 0; + virtual bool parseTagsErrors() const = 0; + virtual ParsedTags parseTags(const QString &source, int statusCode, Site *site) const = 0; virtual PageUrl detailsUrl(qulonglong id, const QString &md5, Site *site) const = 0; - virtual ParsedDetails parseDetails(const QString &source, Site *site) const = 0; + virtual bool parseDetailsErrors() const = 0; + virtual ParsedDetails parseDetails(const QString &source, int statusCode, Site *site) const = 0; virtual PageUrl checkUrl() const = 0; - virtual ParsedCheck parseCheck(const QString &source) const = 0; + virtual bool parseCheckErrors() const = 0; + virtual ParsedCheck parseCheck(const QString &source, int statusCode) const = 0; virtual bool canLoadTags() const = 0; virtual bool canLoadDetails() const = 0; virtual bool canLoadCheck() const = 0; @@ -91,11 +92,10 @@ class Api : public QObject virtual QStringList forcedTokens() const = 0; protected: - QSharedPointer parseImage(Page *parentPage, QMap d, int position, const QList &tags = QList()) const; + QSharedPointer parseImage(Page *parentPage, QMap d, QVariantMap data, int position, const QList &tags = QList()) const; private: QString m_name; - QMap m_data; }; #endif // API_H diff --git a/lib/src/models/api/javascript-api.cpp b/lib/src/models/api/javascript-api.cpp index 0197c13e5..014c85055 100644 --- a/lib/src/models/api/javascript-api.cpp +++ b/lib/src/models/api/javascript-api.cpp @@ -6,6 +6,7 @@ #include "functions.h" #include "logger.h" #include "mixed-settings.h" +#include "models/image.h" #include "models/page.h" #include "models/pool.h" #include "models/site.h" @@ -13,18 +14,6 @@ #include "tags/tag-database.h" -QStringList jsToStringList(const QJSValue &val) -{ - QStringList ret; - - const quint32 length = val.property("length").toUInt(); - for (quint32 i = 0; i < length; ++i) { - ret.append(val.property(i).toString()); - } - - return ret; -} - QString normalize(QString key) { key = key.toLower(); @@ -32,16 +21,15 @@ QString normalize(QString key) return key; } -JavascriptApi::JavascriptApi(const QMap &data, const QJSValue &source, QMutex *jsEngineMutex, const QString &key) - : Api(normalize(key), data), m_source(source), m_key(key), m_engineMutex(jsEngineMutex) +JavascriptApi::JavascriptApi(const QJSValue &source, QMutex *jsEngineMutex, const QString &key) + : Api(normalize(key)), m_source(source), m_key(key), m_engineMutex(jsEngineMutex) {} void JavascriptApi::fillUrlObject(const QJSValue &result, Site *site, PageUrl &ret) const { // Script errors and exceptions - if (result.isError()) - { + if (result.isError()) { const QString err = QStringLiteral("Uncaught exception at line %1: %2").arg(result.property("lineNumber").toInt()).arg(result.toString()); ret.error = err; log(err, Logger::Error); @@ -50,22 +38,19 @@ void JavascriptApi::fillUrlObject(const QJSValue &result, Site *site, PageUrl &r // Parse result QString url; - if (result.isObject()) - { - if (result.hasProperty("error")) - { + if (result.isObject()) { + if (result.hasProperty("error")) { ret.error = result.property("error").toString(); return; } url = result.property("url").toString(); + } else { + url = result.toString(); } - else - { url = result.toString(); } // Site-ize url - if (site != nullptr) - { + if (site != nullptr) { url = site->fixLoginUrl(url); url = site->fixUrl(url).toString(); } @@ -74,15 +59,14 @@ void JavascriptApi::fillUrlObject(const QJSValue &result, Site *site, PageUrl &r } -PageUrl JavascriptApi::pageUrl(const QString &search, int page, int limit, int lastPage, int lastPageMinId, int lastPageMaxId, Site *site) const +PageUrl JavascriptApi::pageUrl(const QString &search, int page, int limit, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId, Site *site) const { PageUrl ret; // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue urlFunction = api.property("search").property("url"); - if (urlFunction.isUndefined()) - { + if (urlFunction.isUndefined()) { ret.error = "This API does not support search"; return ret; } @@ -95,29 +79,15 @@ PageUrl JavascriptApi::pageUrl(const QString &search, int page, int limit, int l opts.setProperty("limit", limit); opts.setProperty("baseUrl", site->baseUrl()); opts.setProperty("loggedIn", site->isLoggedIn(false, true)); - QJSValue auth = m_source.engine()->newObject(); - MixedSettings *settings = site->settings(); - settings->beginGroup("auth"); - const QStringList &authKeys = settings->childKeys(); - for (const QString &key : authKeys) - { - const QString value = settings->value(key).toString(); - if (key == QLatin1String("pseudo") && !auth.hasProperty("login")) - { auth.setProperty("login", value); } - if (key == QLatin1String("password") && !auth.hasProperty("password_hash")) - { auth.setProperty("password_hash", value); } - auth.setProperty(key, value); - } - settings->endGroup(); - opts.setProperty("auth", auth); QJSValue previous = QJSValue(QJSValue::UndefinedValue); - if (lastPage > 0) - { + if (lastPage > 0) { previous = m_source.engine()->newObject(); previous.setProperty("page", lastPage); - previous.setProperty("minId", lastPageMinId); - previous.setProperty("maxId", lastPageMaxId); + previous.setProperty("minIdM1", QString::number(lastPageMinId - 1)); + previous.setProperty("minId", QString::number(lastPageMinId)); + previous.setProperty("maxId", QString::number(lastPageMaxId)); + previous.setProperty("maxIdP1", QString::number(lastPageMaxId + 1)); } const QJSValue result = urlFunction.call(QList() << query << opts << previous); @@ -132,16 +102,15 @@ QList JavascriptApi::makeTags(const QJSValue &tags, Site *site) const QMap tagTypes = site->tagDatabase()->tagTypes(); const quint32 length = tags.property("length").toUInt(); - for (quint32 i = 0; i < length; ++i) - { + for (quint32 i = 0; i < length; ++i) { const QJSValue tag = tags.property(i); - if (tag.isString()) - { + if (tag.isString()) { ret.append(Tag(tag.toString())); continue; } - if (!tag.isObject()) - { continue; } + if (!tag.isObject()) { + continue; + } const int id = tag.hasProperty("id") && !tag.property("id").isUndefined() ? tag.property("id").toInt() : 0; const QString text = tag.property("name").toString(); @@ -149,24 +118,34 @@ QList JavascriptApi::makeTags(const QJSValue &tags, Site *site) const QString type; int typeId = -1; - if (tag.hasProperty("type") && !tag.property("type").isUndefined()) - { - if (tag.property("type").isNumber()) - { typeId = tag.property("type").toInt(); } - else - { type = tag.property("type").toString(); } + if (tag.hasProperty("type") && !tag.property("type").isUndefined()) { + if (tag.property("type").isNumber()) { + typeId = tag.property("type").toInt(); + } else { + type = tag.property("type").toString(); + } + } + if (tag.hasProperty("typeId") && !tag.property("typeId").isUndefined()) { + typeId = tag.property("typeId").toInt(); + } + + QStringList related; + if (tag.hasProperty("related") && !tag.property("related").isUndefined()) { + if (tag.property("related").isArray()) { + related = jsToStringList(tag.property("related")); + } else { + related = tag.property("related").toString().split(' ', QString::SkipEmptyParts); + } } - if (tag.hasProperty("typeId") && !tag.property("typeId").isUndefined()) - { typeId = tag.property("typeId").toInt(); } const TagType tagType = !type.isEmpty() ? TagType(type) : (tagTypes.contains(typeId) ? tagTypes[typeId] : TagType()); - ret.append(Tag(id, text, tagType, count)); + ret.append(Tag(id, text, tagType, count, related)); } return ret; } -ParsedPage JavascriptApi::parsePageInternal(const QString &type, Page *parentPage, const QString &source, int first) const +ParsedPage JavascriptApi::parsePageInternal(const QString &type, Page *parentPage, const QString &source, int statusCode, int first) const { ParsedPage ret; @@ -174,73 +153,88 @@ ParsedPage JavascriptApi::parsePageInternal(const QString &type, Page *parentPag Site *site = parentPage->site(); const QJSValue &api = m_source.property("apis").property(m_key); QJSValue parseFunction = api.property(type).property("parse"); - const QJSValue &results = parseFunction.call(QList() << source); + const QJSValue &results = parseFunction.call(QList() << source << statusCode); // Script errors and exceptions - if (results.isError()) - { + if (results.isError()) { ret.error = QStringLiteral("Uncaught exception at line %1: %2").arg(results.property("lineNumber").toInt()).arg(results.toString()); return ret; } - if (results.hasProperty("error")) - { ret.error = results.property("error").toString(); } + if (results.hasProperty("error")) { + ret.error = results.property("error").toString(); + } - if (results.hasProperty("tags")) - { ret.tags = makeTags(results.property("tags"), site); } + if (results.hasProperty("tags")) { + ret.tags = makeTags(results.property("tags"), site); + } - if (results.hasProperty("images")) - { + if (results.hasProperty("images")) { const QJSValue images = results.property("images"); const quint32 length = images.property("length").toUInt(); - for (quint32 i = 0; i < length; ++i) - { + for (quint32 i = 0; i < length; ++i) { QList tags; + QVariantMap data; QMap d; QJSValueIterator it(images.property(i)); - while (it.hasNext()) - { + while (it.hasNext()) { it.next(); const QString &key = it.name(); const QJSValue &val = it.value(); - if (val.isUndefined()) - { + if (val.isUndefined()) { log(QStringLiteral("Undefined value returned by JS model: %1").arg(key), Logger::Debug); continue; } - if (key == QLatin1String("tags_obj") || (key == QLatin1String("tags") && val.isArray())) - { tags = makeTags(val, site); } - else if (val.isArray()) - { d[key] = jsToStringList(val).join(key == QLatin1String("sources") ? '\n' : ' '); } - else - { d[key] = val.toString(); } + if (key == QLatin1String("tags_obj") || (key == QLatin1String("tags") && val.isArray())) { + tags = makeTags(val, site); + } else if (key == QLatin1String("tokens")) { + QJSValueIterator dit(val); + while (dit.hasNext()) { + dit.next(); + QVariant dval = dit.value().toVariant(); + if (dit.value().isString() && dval.toString().startsWith("date:")) { + const QDateTime date = qDateTimeFromString(dval.toString().mid(5)); + if (date.isValid()) { + dval = date; + } + } + data[dit.name()] = dval; + } + } else if (val.isArray()) { + d[key] = jsToStringList(val).join(key == QLatin1String("sources") ? '\n' : ' '); + } else { + d[key] = val.toString(); + } } - if (!d.isEmpty()) - { - const int id = d["id"].toInt(); - QSharedPointer img = parseImage(parentPage, d, id + first, tags); - if (!img.isNull()) - { ret.images.append(img); } + if (!d.isEmpty()) { + const int pos = first + (d.contains("position") ? d["position"].toInt() : static_cast(i)); + QSharedPointer img = parseImage(parentPage, d, data, pos, tags); + if (!img.isNull()) { + ret.images.append(img); + } } } } // Basic properties - if (results.hasProperty("imageCount") && !results.property("imageCount").isUndefined()) - { ret.imageCount = results.property("imageCount").toInt(); } - if (results.hasProperty("pageCount") && !results.property("pageCount").isUndefined()) - { ret.pageCount = results.property("pageCount").toInt(); } - if (results.hasProperty("urlNextPage") && results.property("urlNextPage").isString()) - { ret.urlNextPage = results.property("urlNextPage").toString(); } - if (results.hasProperty("urlPrevPage") && results.property("urlPrevPage").isString()) - { ret.urlPrevPage = results.property("urlPrevPage").toString(); } - if (results.hasProperty("wiki") && results.property("wiki").isString()) - { + if (results.hasProperty("imageCount") && !results.property("imageCount").isUndefined()) { + ret.imageCount = results.property("imageCount").toInt(); + } + if (results.hasProperty("pageCount") && !results.property("pageCount").isUndefined()) { + ret.pageCount = results.property("pageCount").toInt(); + } + if (results.hasProperty("urlNextPage") && results.property("urlNextPage").isString()) { + ret.urlNextPage = results.property("urlNextPage").toString(); + } + if (results.hasProperty("urlPrevPage") && results.property("urlPrevPage").isString()) { + ret.urlPrevPage = results.property("urlPrevPage").toString(); + } + if (results.hasProperty("wiki") && results.property("wiki").isString()) { ret.wiki = results.property("wiki").toString(); ret.wiki = ret.wiki.replace("href=\"/", "href=\"" + site->baseUrl() + "/"); } @@ -248,27 +242,32 @@ ParsedPage JavascriptApi::parsePageInternal(const QString &type, Page *parentPag return ret; } -ParsedPage JavascriptApi::parsePage(Page *parentPage, const QString &source, int first) const +bool JavascriptApi::parsePageErrors() const { - return parsePageInternal("search", parentPage, source, first); + return getJsConst("search.parseErrors").toBool(); +} + +ParsedPage JavascriptApi::parsePage(Page *parentPage, const QString &source, int statusCode, int first) const +{ + return parsePageInternal("search", parentPage, source, statusCode, first); } -PageUrl JavascriptApi::galleryUrl(const QString &id, int page, int limit, Site *site) const +PageUrl JavascriptApi::galleryUrl(const QSharedPointer &gallery, int page, int limit, Site *site) const { PageUrl ret; // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue urlFunction = api.property("gallery").property("url"); - if (urlFunction.isUndefined()) - { + if (urlFunction.isUndefined()) { ret.error = "This API does not support galleries"; return ret; } QJSValue query = m_source.engine()->newObject(); - query.setProperty("id", id); + query.setProperty("id", QString::number(gallery->id())); + query.setProperty("md5", gallery->md5()); query.setProperty("page", page); QJSValue opts = m_source.engine()->newObject(); @@ -281,9 +280,14 @@ PageUrl JavascriptApi::galleryUrl(const QString &id, int page, int limit, Site * return ret; } -ParsedPage JavascriptApi::parseGallery(Page *parentPage, const QString &source, int first) const +bool JavascriptApi::parseGalleryErrors() const +{ + return getJsConst("gallery.parseErrors").toBool(); +} + +ParsedPage JavascriptApi::parseGallery(Page *parentPage, const QString &source, int statusCode, int first) const { - return parsePageInternal("gallery", parentPage, source, first); + return parsePageInternal("gallery", parentPage, source, statusCode, first); } @@ -302,8 +306,7 @@ PageUrl JavascriptApi::tagsUrl(int page, int limit, Site *site) const // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue urlFunction = api.property("tags").property("url"); - if (urlFunction.isUndefined()) - { + if (urlFunction.isUndefined()) { ret.error = "This API does not support tag loading"; return ret; } @@ -315,21 +318,6 @@ PageUrl JavascriptApi::tagsUrl(int page, int limit, Site *site) const opts.setProperty("limit", limit); opts.setProperty("baseUrl", site->baseUrl()); opts.setProperty("loggedIn", site->isLoggedIn(false, true)); - QJSValue auth = m_source.engine()->newObject(); - MixedSettings *settings = site->settings(); - settings->beginGroup("auth"); - const QStringList &authKeys = settings->childKeys(); - for (const QString &key : authKeys) - { - const QString value = settings->value(key).toString(); - if (key == QLatin1String("pseudo") && !auth.hasProperty("login")) - { auth.setProperty("login", value); } - if (key == QLatin1String("password") && !auth.hasProperty("password_hash")) - { auth.setProperty("password_hash", value); } - auth.setProperty(key, value); - } - settings->endGroup(); - opts.setProperty("auth", auth); const QJSValue result = urlFunction.call(QList() << query << opts); fillUrlObject(result, site, ret); @@ -337,26 +325,32 @@ PageUrl JavascriptApi::tagsUrl(int page, int limit, Site *site) const return ret; } -ParsedTags JavascriptApi::parseTags(const QString &source, Site *site) const +bool JavascriptApi::parseTagsErrors() const +{ + return getJsConst("tags.parseErrors").toBool(); +} + +ParsedTags JavascriptApi::parseTags(const QString &source, int statusCode, Site *site) const { ParsedTags ret; // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue parseFunction = api.property("tags").property("parse"); - QJSValue results = parseFunction.call(QList() << source); + QJSValue results = parseFunction.call(QList() << source << statusCode); // Script errors and exceptions - if (results.isError()) - { + if (results.isError()) { ret.error = QStringLiteral("Uncaught exception at line %1: %2").arg(results.property("lineNumber").toInt()).arg(results.toString()); return ret; } - if (results.hasProperty("error")) - { ret.error = results.property("error").toString(); } - if (results.hasProperty("tags")) - { ret.tags = makeTags(results.property("tags"), site); } + if (results.hasProperty("error")) { + ret.error = results.property("error").toString(); + } + if (results.hasProperty("tags")) { + ret.tags = makeTags(results.property("tags"), site); + } return ret; } @@ -377,8 +371,7 @@ PageUrl JavascriptApi::detailsUrl(qulonglong id, const QString &md5, Site *site) // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue urlFunction = api.property("details").property("url"); - if (urlFunction.isUndefined()) - { + if (urlFunction.isUndefined()) { ret.error = "This API does not support details loading"; return ret; } @@ -389,40 +382,47 @@ PageUrl JavascriptApi::detailsUrl(qulonglong id, const QString &md5, Site *site) return ret; } -ParsedDetails JavascriptApi::parseDetails(const QString &source, Site *site) const +bool JavascriptApi::parseDetailsErrors() const +{ + return getJsConst("details.parseErrors").toBool(); +} + +ParsedDetails JavascriptApi::parseDetails(const QString &source, int statusCode, Site *site) const { ParsedDetails ret; // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue parseFunction = api.property("details").property("parse"); - QJSValue results = parseFunction.call(QList() << source); + QJSValue results = parseFunction.call(QList() << source << statusCode); // Script errors and exceptions - if (results.isError()) - { + if (results.isError()) { ret.error = QStringLiteral("Uncaught exception at line %1: %2").arg(results.property("lineNumber").toInt()).arg(results.toString()); return ret; } - if (results.hasProperty("error") && results.property("error").isString()) - { ret.error = results.property("error").toString(); } - if (results.hasProperty("tags")) - { ret.tags = makeTags(results.property("tags"), site); } - if (results.hasProperty("imageUrl") && results.property("imageUrl").isString()) - { ret.imageUrl = results.property("imageUrl").toString(); } - if (results.hasProperty("createdAt") && results.property("createdAt").isString()) - { ret.createdAt = qDateTimeFromString(results.property("createdAt").toString()); } + if (results.hasProperty("error") && results.property("error").isString()) { + ret.error = results.property("error").toString(); + } + if (results.hasProperty("tags")) { + ret.tags = makeTags(results.property("tags"), site); + } + if (results.hasProperty("imageUrl") && results.property("imageUrl").isString()) { + ret.imageUrl = results.property("imageUrl").toString(); + } + if (results.hasProperty("createdAt") && results.property("createdAt").isString()) { + ret.createdAt = qDateTimeFromString(results.property("createdAt").toString()); + } - if (results.hasProperty("pools")) - { + if (results.hasProperty("pools")) { const QJSValue pools = results.property("pools"); const quint32 length = pools.property("length").toUInt(); - for (quint32 i = 0; i < length; ++i) - { + for (quint32 i = 0; i < length; ++i) { const QJSValue pool = pools.property(i); - if (!pool.isObject()) + if (!pool.isObject()) { continue; + } const int id = pool.hasProperty("id") ? pool.property("id").toInt() : 0; const QString name = pool.property("name").toString(); @@ -452,8 +452,7 @@ PageUrl JavascriptApi::checkUrl() const // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue urlFunction = api.property("check").property("url"); - if (urlFunction.isUndefined()) - { + if (urlFunction.isUndefined()) { ret.error = "This API does not support checking"; return ret; } @@ -464,18 +463,22 @@ PageUrl JavascriptApi::checkUrl() const return ret; } -ParsedCheck JavascriptApi::parseCheck(const QString &source) const +bool JavascriptApi::parseCheckErrors() const +{ + return getJsConst("check.parseErrors").toBool(); +} + +ParsedCheck JavascriptApi::parseCheck(const QString &source, int statusCode) const { ParsedCheck ret; // QMutexLocker locker(m_engineMutex); QJSValue api = m_source.property("apis").property(m_key); QJSValue parseFunction = api.property("check").property("parse"); - QJSValue result = parseFunction.call(QList() << source); + QJSValue result = parseFunction.call(QList() << source << statusCode); // Script errors and exceptions - if (result.isError()) - { + if (result.isError()) { ret.error = QStringLiteral("Uncaught exception at line %1: %2").arg(result.property("lineNumber").toInt()).arg(result.toString()); ret.ok = false; return ret; @@ -487,14 +490,31 @@ ParsedCheck JavascriptApi::parseCheck(const QString &source) const } -QJSValue JavascriptApi::getJsConst(const QString &key, const QJSValue &def) const +static QJSValue tryGetValue(QJSValue api, const QStringList &properties) +{ + for (int i = 0; !api.isUndefined() && i < properties.length(); ++i) { + api = api.property(properties[i]); + } + return api; +} + +QJSValue JavascriptApi::getJsConst(const QString &fullKey, const QJSValue &def) const { // QMutexLocker locker(m_engineMutex); + + const QStringList properties = fullKey.split('.'); + QJSValue api = m_source.property("apis").property(m_key); - if (api.hasProperty(key)) - { return api.property(key); } - if (m_source.hasProperty(key)) - { return m_source.property(key); } + QJSValue fromApi = tryGetValue(api, properties); + if (!fromApi.isUndefined()) { + return fromApi; + } + + QJSValue fromSource = tryGetValue(m_source, properties); + if (!fromSource.isUndefined()) { + return fromSource; + } + return def; } diff --git a/lib/src/models/api/javascript-api.h b/lib/src/models/api/javascript-api.h index 13c262b70..2049eee17 100644 --- a/lib/src/models/api/javascript-api.h +++ b/lib/src/models/api/javascript-api.h @@ -15,19 +15,24 @@ class JavascriptApi : public Api Q_OBJECT public: - explicit JavascriptApi(const QMap &data, const QJSValue &source, QMutex *jsEngineMutex, const QString &key); + explicit JavascriptApi(const QJSValue &source, QMutex *jsEngineMutex, const QString &key); // API - PageUrl pageUrl(const QString &search, int page, int limit, int lastPage, int lastPageMinId, int lastPageMaxId, Site *site) const override; - ParsedPage parsePage(Page *parentPage, const QString &source, int first) const override; - PageUrl galleryUrl(const QString &id, int page, int limit, Site *site) const override; - ParsedPage parseGallery(Page *parentPage, const QString &source, int first) const override; + PageUrl pageUrl(const QString &search, int page, int limit, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId, Site *site) const override; + bool parsePageErrors() const override; + ParsedPage parsePage(Page *parentPage, const QString &source, int statusCode, int first) const override; + PageUrl galleryUrl(const QSharedPointer &gallery, int page, int limit, Site *site) const override; + bool parseGalleryErrors() const override; + ParsedPage parseGallery(Page *parentPage, const QString &source, int statusCode, int first) const override; PageUrl tagsUrl(int page, int limit, Site *site) const override; - ParsedTags parseTags(const QString &source, Site *site) const override; + bool parseTagsErrors() const override; + ParsedTags parseTags(const QString &source, int statusCode, Site *site) const override; PageUrl detailsUrl(qulonglong id, const QString &md5, Site *site) const override; - ParsedDetails parseDetails(const QString &source, Site *site) const override; + bool parseDetailsErrors() const override; + ParsedDetails parseDetails(const QString &source, int statusCode, Site *site) const override; PageUrl checkUrl() const override; - ParsedCheck parseCheck(const QString &source) const override; + bool parseCheckErrors() const override; + ParsedCheck parseCheck(const QString &source, int statusCode) const override; bool needAuth() const override; bool canLoadTags() const override; bool canLoadDetails() const override; @@ -41,7 +46,7 @@ class JavascriptApi : public Api void fillUrlObject(const QJSValue &result, Site *site, PageUrl &ret) const; QList makeTags(const QJSValue &tags, Site *site) const; QJSValue getJsConst(const QString &key, const QJSValue &def = QJSValue(QJSValue::UndefinedValue)) const; - ParsedPage parsePageInternal(const QString &type, Page *parentPage, const QString &source, int first) const; + ParsedPage parsePageInternal(const QString &type, Page *parentPage, const QString &source, int statusCode, int first) const; private: const QJSValue &m_source; diff --git a/lib/src/models/api/javascript-grabber-helper.cpp b/lib/src/models/api/javascript-grabber-helper.cpp index 132299624..ad3ef67a6 100644 --- a/lib/src/models/api/javascript-grabber-helper.cpp +++ b/lib/src/models/api/javascript-grabber-helper.cpp @@ -24,32 +24,34 @@ QJSValue JavascriptGrabberHelper::regexMatches(const QString ®ex, const QStri auto matches = reg.globalMatch(txt); quint32 matchI = 0; - while (matches.hasNext()) - { + while (matches.hasNext()) { QJSValue jsMatch = m_engine.newObject(); auto match = matches.next(); - for (QString group : groups) - { - if (group.isEmpty()) + for (QString group : groups) { + if (group.isEmpty()) { continue; + } QString val = match.captured(group); - if (val.isEmpty()) + if (val.isEmpty()) { continue; + } const int underscorePos = group.lastIndexOf('_'); bool ok; group.midRef(underscorePos + 1).toInt(&ok); - if (underscorePos != -1 && ok) - { group = group.left(underscorePos); } + if (underscorePos != -1 && ok) { + group = group.left(underscorePos); + } jsMatch.setProperty(group, val); } const QStringList &caps = match.capturedTexts(); - for (int i = 0; i < caps.count(); ++i) - { jsMatch.setProperty(i, match.captured(i)); } + for (int i = 0; i < caps.count(); ++i) { + jsMatch.setProperty(i, match.captured(i)); + } ret.setProperty(matchI++, jsMatch); } @@ -63,15 +65,17 @@ QJSValue JavascriptGrabberHelper::_parseXMLRec(const QDomNode &node) const const auto type = node.nodeType(); + // Text node + if (type == QDomNode::TextNode || type == QDomNode::CDATASectionNode) { + return node.nodeValue(); + } + // Element node - if (type == QDomNode::ElementNode) - { + if (type == QDomNode::ElementNode) { const QDomNamedNodeMap &attributes = node.attributes(); - if (attributes.count() > 0) - { + if (attributes.count() > 0) { QJSValue attr = m_engine.newObject(); - for (int j = 0; j < attributes.count(); j++) - { + for (int j = 0; j < attributes.count(); j++) { QDomNode attribute = attributes.item(j); attr.setProperty(attribute.nodeName(), attribute.nodeValue()); } @@ -79,26 +83,18 @@ QJSValue JavascriptGrabberHelper::_parseXMLRec(const QDomNode &node) const } } - // Text node - else if (type == QDomNode::TextNode || type == QDomNode::CDATASectionNode) - { obj = node.nodeValue(); } - // Children - if (node.hasChildNodes()) - { + if (node.hasChildNodes()) { const QDomNodeList &children = node.childNodes(); - for (int i = 0; i < children.count(); i++) - { + for (int i = 0; i < children.count(); i++) { const QDomNode &item = children.item(i); const QString &nodeName = item.nodeName(); - if (obj.property(nodeName).isUndefined()) - { obj.setProperty(nodeName, _parseXMLRec(item)); } - else - { + if (obj.property(nodeName).isUndefined()) { + obj.setProperty(nodeName, _parseXMLRec(item)); + } else { QJSValue prop = obj.property(nodeName); - if (!prop.isArray()) - { + if (!prop.isArray()) { QJSValue newProp = m_engine.newArray(); newProp.setProperty(0, prop); obj.setProperty(nodeName, newProp); @@ -119,8 +115,7 @@ QJSValue JavascriptGrabberHelper::parseXML(const QString &txt) const QDomDocument doc; QString errorMsg; int errorLine, errorColumn; - if (!doc.setContent(txt, false, &errorMsg, &errorLine, &errorColumn)) - { + if (!doc.setContent(txt, false, &errorMsg, &errorLine, &errorColumn)) { log(QStringLiteral("Error parsing XML file: %1 (%2 - %3).").arg(errorMsg, QString::number(errorLine), QString::number(errorColumn)), Logger::Error); return QJSValue(QJSValue::UndefinedValue); } diff --git a/lib/src/models/favorite.cpp b/lib/src/models/favorite.cpp index 5b68677b2..d200137a3 100644 --- a/lib/src/models/favorite.cpp +++ b/lib/src/models/favorite.cpp @@ -22,8 +22,9 @@ void Favorite::setNote(int note) QString Favorite::getName(bool clean) const { - if (clean) + if (clean) { return QString(m_name).remove('\\').remove('/').remove(':').remove('*').remove('?').remove('"').remove('<').remove('>').remove('|'); + } return m_name; } int Favorite::getNote() const @@ -37,8 +38,9 @@ QList &Favorite::getMonitors() bool Favorite::setImage(const QPixmap &img) { - if (!QDir(savePath("thumbs")).exists()) + if (!QDir(savePath("thumbs")).exists()) { QDir(savePath()).mkdir("thumbs"); + } m_imagePath = savePath("thumbs/" + getName(true) + ".png"); return img @@ -48,8 +50,7 @@ bool Favorite::setImage(const QPixmap &img) QPixmap Favorite::getImage() const { QPixmap img(m_imagePath); - if (img.width() > 150 || img.height() > 150) - { + if (img.width() > 150 || img.height() > 150) { img = img.scaled(QSize(150, 150), Qt::KeepAspectRatio, Qt::SmoothTransformation); img.save(savePath("thumbs/" + getName(true) + ".png"), "PNG"); } @@ -71,8 +72,9 @@ Favorite Favorite::fromString(const QString &path, const QString &text) : QDateTime::fromString(xp.takeFirst(), Qt::ISODate); QString thumbPath = path + "/thumbs/" + (QString(tag).remove('\\').remove('/').remove(':').remove('*').remove('?').remove('"').remove('<').remove('>').remove('|')) + ".png"; - if (!QFile::exists(thumbPath)) + if (!QFile::exists(thumbPath)) { thumbPath = ":/images/noimage.png"; + } return Favorite(tag, note, lastViewed, thumbPath); } @@ -84,8 +86,7 @@ void Favorite::toJson(QJsonObject &json) const json["lastViewed"] = getLastViewed().toString(Qt::ISODate); QJsonArray monitorsJson; - for (const Monitor &monitor : m_monitors) - { + for (const Monitor &monitor : m_monitors) { QJsonObject obj; monitor.toJson(obj); monitorsJson.append(obj); @@ -99,13 +100,15 @@ Favorite Favorite::fromJson(const QString &path, const QJsonObject &json, const const QDateTime lastViewed = QDateTime::fromString(json["lastViewed"].toString(), Qt::ISODate); QString thumbPath = path + "/thumbs/" + (QString(tag).remove('\\').remove('/').remove(':').remove('*').remove('?').remove('"').remove('<').remove('>').remove('|')) + ".png"; - if (!QFile::exists(thumbPath)) + if (!QFile::exists(thumbPath)) { thumbPath = ":/images/noimage.png"; + } QList monitors; QJsonArray monitorsJson = json["monitors"].toArray(); - for (auto monitorJson : monitorsJson) - { monitors.append(Monitor::fromJson(monitorJson.toObject(), sites)); } + for (auto monitorJson : monitorsJson) { + monitors.append(Monitor::fromJson(monitorJson.toObject(), sites)); + } return Favorite(tag, note, lastViewed, monitors, thumbPath); } diff --git a/lib/src/models/filename.cpp b/lib/src/models/filename.cpp index dcbcc1c2d..fe955630e 100644 --- a/lib/src/models/filename.cpp +++ b/lib/src/models/filename.cpp @@ -4,6 +4,10 @@ #include #include #include +#include "filename/ast/filename-node-variable.h" +#include "filename/ast-filename.h" +#include "filename/filename-cache.h" +#include "filename/filename-execution-visitor.h" #include "functions.h" #include "loader/token.h" #include "models/api/api.h" @@ -13,221 +17,92 @@ #include "models/site.h" +Filename::Filename() + : Filename("") +{} + Filename::Filename(QString format) : m_format(std::move(format)) -{ } - -QString Filename::expandConditionals(const QString &text, const QStringList &tags, const QMap &tokens, QSettings *settings, int depth) const { - QString ret = text; - - QRegularExpression reg(QStringLiteral("\\<([^>]+)\\>")); - auto matches = reg.globalMatch(text); - while (matches.hasNext()) - { - auto match = matches.next(); - QString cap = match.captured(1); - if (!cap.isEmpty() && !cap.startsWith('<')) - { - cap += QStringLiteral(">").repeated(cap.count('<') - cap.count('>')); - ret.replace("<" + cap + ">", this->expandConditionals(cap, tags, tokens, settings, depth + 1)); - } - } - - if (depth > 0) - { - // Token-based conditions - reg = QRegularExpression(QStringLiteral("(-)?(!)?(%([^:%]+)(?::[^%]+)?%)")); - matches = reg.globalMatch(text); - while (matches.hasNext()) - { - const auto match = matches.next(); - const bool ignore = !match.captured(1).isEmpty(); - const bool invert = !match.captured(2).isEmpty(); - const QString &fullToken = match.captured(3); - const QString &token = match.captured(4); - if ((tokens.contains(token) && !isVariantEmpty(tokens[token].value())) == !invert) - { - const QString &rep = ignore || invert ? QString() : fullToken; - ret.replace(match.captured(0), rep); - } - else - { return QString(); } - } - - // Tag-based conditions - reg = QRegularExpression(QStringLiteral("(-)?(!)?\"([^\"]+)\"")); - matches = reg.globalMatch(text); - while (matches.hasNext()) - { - const auto match = matches.next(); - const bool ignore = !match.captured(1).isEmpty(); - const bool invert = !match.captured(2).isEmpty(); - const QString &tag = match.captured(3); - if (tags.contains(tag, Qt::CaseInsensitive) == !invert) - { - const QString &rep = ignore ? QString() : this->cleanUpValue(tag, QMap(), settings); - ret.replace(match.captured(0), rep); - } - else - { return QString(); } - } - } - - if (depth == 0) - { ret.replace(QRegularExpression(QStringLiteral("<<([^>]*)>>")), QStringLiteral("<\\1>")); } - - return ret; + m_ast = FilenameCache::Get(m_format); } QList Filename::getReplace(const QString &key, const Token &token, QSettings *settings) const { QList ret; - const QStringList &value = token.value().toStringList(); + QStringList value = token.value().toStringList(); - if (token.whatToDoDefault().isEmpty()) - { + if (token.whatToDoDefault().isEmpty()) { ret.append(Token(value)); return ret; } settings->beginGroup("Save"); - if (value.isEmpty()) - { ret.append(Token(settings->value(key + "_empty", token.emptyDefault()).toString())); } - else if (value.size() > settings->value(key + "_multiple_limit", 1).toInt()) - { + const QString sort = settings->value(key + "_sort", "original").toString(); + if (sort == QLatin1String("name")) { + value.sort(); + } + + if (value.isEmpty()) { + ret.append(Token(settings->value(key + "_empty", token.emptyDefault()).toString())); + } else if (value.size() > settings->value(key + "_multiple_limit", 1).toInt()) { const QString &whatToDo = settings->value(key + "_multiple", token.whatToDoDefault()).toString(); - if (whatToDo == QLatin1String("keepAll")) - { ret.append(Token(value)); } - else if (whatToDo == QLatin1String("multiple")) - { + if (whatToDo == QLatin1String("keepAll")) { + ret.append(Token(value)); + } else if (whatToDo == QLatin1String("multiple")) { ret.reserve(ret.count() + value.count()); - for (const QString &val : value) - { ret.append(Token(val)); } - } - else if (whatToDo == QLatin1String("keepN")) - { + for (const QString &val : value) { + ret.append(Token(val)); + } + } else if (whatToDo == QLatin1String("keepN")) { const int keepN = settings->value(key + "_multiple_keepN", 1).toInt(); ret.append(Token(QStringList(value.mid(0, qMax(1, keepN))))); - } - else if (whatToDo == QLatin1String("keepNThenAdd")) - { + } else if (whatToDo == QLatin1String("keepNThenAdd")) { const int keepN = settings->value(key + "_multiple_keepNThenAdd_keep", 1).toInt(); QString thenAdd = settings->value(key + "_multiple_keepNThenAdd_add", " (+ %count%)").toString(); thenAdd.replace("%total%", QString::number(value.size())); thenAdd.replace("%count%", QString::number(value.size() - keepN)); QStringList keptValues = value.mid(0, qMax(1, keepN)); - if (value.size() > keepN) - { ret.append(Token(keptValues.join(' ') + thenAdd)); } - else - { ret.append(Token(keptValues)); } + if (value.size() > keepN) { + ret.append(Token(keptValues.join(' ') + thenAdd)); + } else { + ret.append(Token(keptValues)); + } + } else { + ret.append(Token(settings->value(key + "_value", token.multipleDefault()).toString())); } - else - { ret.append(Token(settings->value(key + "_value", token.multipleDefault()).toString())); } + } else { + ret.append(Token(value)); } - else - { ret.append(Token(value)); } settings->endGroup(); return ret; } -void Filename::setJavaScriptVariables(QJSEngine &engine, QSettings *settings, const QMap &tokens) const -{ - QJSValue obj = engine.globalObject(); - - for (auto it = tokens.constBegin(); it != tokens.constEnd(); ++it) - { - const QString &key = it.key(); - QVariant val = it.value().value(); - - if (val.type() == QVariant::StringList || val.type() == QVariant::String) - { - QString res; - - if (val.type() == QVariant::StringList) - { - QStringList vals = val.toStringList(); - const QString mainSeparator = settings->value("Save/separator", " ").toString(); - const QString tagSeparator = fixSeparator(settings->value("Save/" + key + "_sep", mainSeparator).toString()); - - if (key != "all" && key != "tags") - { obj.setProperty(key + "s", engine.toScriptValue(vals)); } - - res = vals.join(tagSeparator); - } - else - { res = val.toString(); } - - if (key != "allo") - { - res = res.replace("\\", "_").replace("%", "_").replace("/", "_").replace(":", "_").replace("|", "_").replace("*", "_").replace("?", "_").replace("\"", "_").replace("<", "_").replace(">", "_").replace("__", "_").replace("__", "_").replace("__", "_").trimmed(); - if (!settings->value("Save/replaceblanks", false).toBool()) - { res.replace("_", " "); } - } - - obj.setProperty(key, res); - } - else - { obj.setProperty(key, engine.toScriptValue(val)); } - } -} - -bool Filename::matchConditionalFilename(QString cond, QSettings *settings, const QMap &tokens) const -{ - if (cond.isEmpty()) - return false; - - // Javascript conditions - if (cond.startsWith("javascript:")) - { - // We remove the "javascript:" part - cond = cond.right(cond.length() - 11); - - // Script execution - QJSEngine engine; - setJavaScriptVariables(engine, settings, tokens); - QJSValue result = engine.evaluate(cond); - if (result.isError()) - { - log("Error in Javascript evaluation:
" + result.toString()); - return false; - } - - return result.toBool(); - } - - const PostFilter filter(cond.split(' ')); - const QStringList matches = filter.match(tokens); - - return matches.count() < filter.count(); -} - -QList> Filename::expandTokens(const QString &filename, QMap tokens, QSettings *settings) const +QList> Filename::expandTokens(QMap tokens, QSettings *settings) const { QList> ret; ret.append(tokens); - const bool isJavascript = filename.startsWith(QLatin1String("javascript:")); - for (const QString &key : tokens.keys()) - { + const bool isJavascript = m_format.startsWith(QLatin1String("javascript:")); + for (const QString &key : tokens.keys()) { const Token &token = tokens[key]; - if (token.value().type() != QVariant::StringList) + if (token.value().type() != QVariant::StringList) { continue; + } - const bool hasToken = !isJavascript && filename.contains(QRegularExpression("%" + key + "(?::[^%]+)?%")); - const bool hasVar = isJavascript && filename.contains(key); - if (!hasToken && !hasVar) + const bool hasToken = !isJavascript && m_ast->tokens().contains(key); + const bool hasVar = isJavascript && m_format.contains(key); + if (!hasToken && !hasVar) { continue; + } QList reps = getReplace(key, token, settings); const int cnt = ret.count(); - for (int i = 0; i < cnt; ++i) - { + for (int i = 0; i < cnt; ++i) { ret[i].insert(key, reps[0]); - for (int j = 1; j < reps.count(); ++j) - { + for (int j = 1; j < reps.count(); ++j) { tokens = ret[i]; tokens.insert(key, reps[j]); ret.append(tokens); @@ -238,165 +113,121 @@ QList> Filename::expandTokens(const QString &filename, QMap return ret; } -QStringList Filename::path(const Image &img, Profile *profile, const QString &pth, int counter, bool complex, bool maxLength, bool shouldFixFilename, bool getFull, bool keepInvalidTokens) const -{ return path(img.tokens(profile), profile, pth, counter, complex, maxLength, shouldFixFilename, getFull, keepInvalidTokens); } -QStringList Filename::path(QMap tokens, Profile *profile, QString folder, int counter, bool complex, bool maxLength, bool shouldFixFilename, bool getFull, bool keepInvalidTokens) const +QStringList Filename::path(const Image &img, Profile *profile, const QString &pth, int counter, PathFlags flags) const +{ return path(img.tokens(profile), profile, pth, counter, flags); } +QStringList Filename::path(QMap tokens, Profile *profile, QString folder, int counter, PathFlags flags) const { QSettings *settings = profile->getSettings(); - QString filename = m_format; // Count token tokens.insert("count", Token(counter)); // Conditional filenames - if (complex) - { - QMap> filenames = getFilenames(settings); - for (auto it = filenames.constBegin(); it != filenames.constEnd(); ++it) - { - if (matchConditionalFilename(it.key(), settings, tokens)) - { - const QPair &result = it.value(); - if (!result.first.isEmpty()) - { filename = result.first; } - if (!result.second.isEmpty()) - { folder = result.second; } + if (flags.testFlag(PathFlag::ConditionalFilenames)) { + QList filenames = getFilenames(settings); + for (const auto &fn : filenames) { + if (fn.matches(tokens, settings)) { + if (!fn.path.isEmpty()) { + folder = fn.path; + } + if (!fn.filename.format().isEmpty()) { + return fn.filename.path(tokens, profile, folder, counter, flags & (~PathFlag::ConditionalFilenames)); + } } } } - // Expand tokens into multiple filenames - QList> replacesList = expandTokens(filename, tokens, settings); - QStringList fns; - if (filename.startsWith("javascript:")) - { - // We remove the "javascript:" part - filename = filename.right(filename.length() - 11); - - for (const auto &replaces : replacesList) - { - // Script execution - QJSEngine engine; - setJavaScriptVariables(engine, settings, replaces); - QJSValue result = engine.evaluate(filename); - if (result.isError()) - { - log("Error in Javascript evaluation:
" + result.toString()); - return QStringList(); - } + if (m_format.isEmpty()) { + return fns; + } - fns.append(result.toString()); + // Expand tokens into multiple filenames + QList> replacesList = expandTokens(tokens, settings); + + for (const auto &replaces : replacesList) { + FilenameExecutionVisitor executionVisitor(replaces, settings); + executionVisitor.setEscapeMethod(m_escapeMethod); + executionVisitor.setKeepInvalidTokens(flags.testFlag(PathFlag::KeepInvalidTokens)); + // TODO(Bionus): PathFlag::ExpandConditionals + QString cFilename = executionVisitor.run(*m_ast->ast()); + + // Something wrong happened (JavaScript error...) + if (cFilename.isEmpty()) { + continue; } - } - else if (!filename.isEmpty()) - { - // We get path and remove useless slashes from filename - folder.replace("\\", "/"); - // filename.replace("\\", "/"); - if (filename.at(0) == QChar('/')) - { filename = filename.right(filename.length() - 1); } - if (folder.right(1) == "/") - { folder = folder.left(folder.length() - 1); } - - QStringList ignoredTokens = QStringList() << "path" << "num"; - - for (const auto &replaces : replacesList) - { - QString cFilename = QString(filename); - QString hasNum; - QString numOptions; - const QStringList namespaces = replaces["all_namespaces"].value().toStringList(); - - // Conditionals - if (complex) - { cFilename = this->expandConditionals(cFilename, tokens["allos"].value().toStringList(), replaces, settings); } - - // Replace everything - QRegExp replacerx("%([^:]+)(?::([^%]+))?%"); - replacerx.setMinimal(true); - int p = 0; - while ((p = replacerx.indexIn(cFilename, p)) != -1) - { - const QString &key = replacerx.cap(1); - const QString &options = replacerx.captureCount() > 1 ? replacerx.cap(2) : QString(); - - if (replaces.contains(key)) - { - const QVariant &val = replaces[key].value(); - const QString &res = optionedValue(val, key, options, settings, namespaces); - cFilename.replace(replacerx.cap(0), res); - p += res.length(); - } - else if (ignoredTokens.contains(key)) - { - if (key == "num") - { - hasNum = replacerx.cap(0); - numOptions = options; - } - - p += replacerx.matchedLength(); + + // "num" special token + QRegularExpression rxNum("%num(?::.+)?%"); + auto numMatch = rxNum.match(cFilename); + if (numMatch.hasMatch()) { + FilenameParser parser(numMatch.captured()); + FilenameNodeVariable *var = parser.parseVariable(); + + int num = 1; + const QString hasNum = numMatch.captured(); + const bool noExt = var->opts.contains("noext"); + + const int mid = QDir::toNativeSeparators(cFilename).lastIndexOf(QDir::separator()); + QDir dir(folder + (mid >= 0 ? QDir::separator() + cFilename.left(mid) : QString())); + const QString cRight = mid >= 0 ? cFilename.right(cFilename.length() - mid - 1) : cFilename; + QString filter = QString(cRight).replace(hasNum, "*"); + if (noExt) { + const QString ext = replaces["ext"].toString(); + if (filter.endsWith("." + ext)) { + filter = filter.left(filter.length() - ext.length()) + "*"; } - else if (!keepInvalidTokens) - { cFilename.remove(replacerx.cap(0)); } - else - { p += replacerx.matchedLength(); } } + QFileInfoList files = dir.entryInfoList(QStringList() << filter, QDir::Files, QDir::NoSort); - if (!hasNum.isEmpty()) - { - const int mid = QDir::toNativeSeparators(cFilename).lastIndexOf(QDir::separator()); - QDir dir(folder + (mid >= 0 ? QDir::separator() + cFilename.left(mid) : QString())); - const QString cRight = mid >= 0 ? cFilename.right(cFilename.length() - mid - 1) : cFilename; - const QString filter = QString(cRight).replace(hasNum, "*"); - QFileInfoList files = dir.entryInfoList(QStringList() << filter, QDir::Files, QDir::NoSort); - - // Sort files naturally + if (!files.isEmpty()) { + // Get last file QCollator collator; collator.setNumericMode(true); - std::sort( - files.begin(), - files.end(), - [&collator](const QFileInfo &a, const QFileInfo &b) { return collator.compare(a.fileName(), b.fileName()) < 0; } - ); - - int num = 1; - if (!files.isEmpty()) - { - const QString last = files.last().fileName(); - const int pos = cRight.indexOf(hasNum); - const int len = last.length() - cRight.length() + 5; - num = last.midRef(pos, len).toInt() + 1; - } - cFilename.replace(hasNum, optionedValue(num, "num", numOptions, settings, namespaces)); + const QFileInfo highest = noExt + ? *std::max_element( + files.begin(), + files.end(), + [&collator](const QFileInfo &a, const QFileInfo &b) { return collator.compare(a.completeBaseName(), b.completeBaseName()) < 0; } + ) + : *std::max_element( + files.begin(), + files.end(), + [&collator](const QFileInfo &a, const QFileInfo &b) { return collator.compare(a.fileName(), b.fileName()) < 0; } + ); + + const QString last = highest.fileName(); + const int pos = cRight.indexOf(hasNum); + const int len = last.length() - cRight.length() + hasNum.length(); + num = last.midRef(pos, len).toInt() + 1; } - fns.append(cFilename); + QString val = executionVisitor.variableToString(var->name, num, var->opts); + cFilename.replace(numMatch.capturedStart(), numMatch.capturedLength(), val); } + + fns.append(cFilename); } const int cnt = fns.count(); - for (int i = 0; i < cnt; ++i) - { - if (shouldFixFilename) - { + for (int i = 0; i < cnt; ++i) { + if (flags.testFlag(PathFlag::Fix)) { // Trim directory names fns[i] = fns[i].trimmed(); fns[i].replace(QRegularExpression(" */ *"), "/"); // Max filename size option - const int limit = !maxLength ? 0 : settings->value("Save/limit").toInt(); + const int limit = !flags.testFlag(PathFlag::CapLength) ? 0 : settings->value("Save/limit").toInt(); fns[i] = fixFilename(fns[i], folder, limit); } // Include directory in result - if (getFull) - { fns[i] = folder + "/" + fns[i]; } + if (flags.testFlag(PathFlag::IncludeFolder)) { + fns[i] = folder + "/" + fns[i]; + } - if (shouldFixFilename) - { + if (flags.testFlag(PathFlag::Fix)) { // Native separators fns[i] = QDir::toNativeSeparators(fns[i]); @@ -409,132 +240,7 @@ QStringList Filename::path(QMap tokens, Profile *profile, QStrin return fns; } -QString Filename::cleanUpValue(QString res, const QMap &options, QSettings *settings) const -{ - // Forbidden characters - if (!options.contains("unsafe")) - { res = res.replace("\\", "_").replace("%", "_").replace("/", "_").replace(":", "_").replace("|", "_").replace("*", "_").replace("?", "_").replace("\"", "_").replace("<", "_").replace(">", "_").replace("__", "_").replace("__", "_").replace("__", "_").trimmed(); } - - // Replace underscores by spaces - if (!options.contains("underscores") && (!settings->value("Save/replaceblanks", false).toBool() || options.contains("spaces"))) - { res = res.replace("_", " "); } - - return res; -} - -QString Filename::optionedValue(const QVariant &val, const QString &key, const QString &ops, QSettings *settings, QStringList namespaces) const -{ - bool cleaned = false; - - // Parse options - QMap options; - if (!ops.isEmpty()) - { - QStringList opts = ops.split(QRegularExpression("(?value("Save/separator", " ").toString(); - QString tagSeparator = fixSeparator(settings->value("Save/" + key + "_sep", mainSeparator).toString()); - - // Namespaces - if (options.contains("ignorenamespace")) - { - QStringList ignored = options["ignorenamespace"].split(' '); - QStringList filtered, filteredNamespaces; - for (int i = 0; i < vals.count(); ++i) - { - const QString &nspace = key == "all" ? namespaces[i] : key; - if (!ignored.contains(nspace)) - { - filtered.append(vals[i]); - filteredNamespaces.append(namespaces[i]); - } - } - vals = filtered; - namespaces = filteredNamespaces; - } - if (options.contains("includenamespace")) - { - QStringList excluded; - if (options.contains("excludenamespace")) - { excluded = options["excludenamespace"].split(' '); } - - QStringList namespaced; - for (int i = 0; i < vals.count(); ++i) - { - const QString nspace = key == "all" ? namespaces[i] : key; - namespaced.append((!excluded.contains(nspace) ? nspace + ":" : QString()) + vals[i]); - } - vals = namespaced; - } - if (options.contains("separator")) - { tagSeparator = fixSeparator(options["separator"]); } - if (options.contains("sort")) - { std::sort(vals.begin(), vals.end()); } - - // Clean each value separately - if (!key.startsWith("source")) - { - for (QString &t : vals) - { t = this->cleanUpValue(t, options, settings); } - cleaned = true; - } - - res = vals.join(tagSeparator); - } - else - { res = val.toString(); } - - // String options - if (options.contains("maxlength")) - { res = res.left(options["maxlength"].toInt()); } - if (options.contains("htmlescape")) - { res = res.toHtmlEscaped(); } - - // Forbidden characters and spaces replacement settings - if (key != "allo" && !key.startsWith("url_") && key != "filename" && !key.startsWith("source") && !cleaned) - { res = this->cleanUpValue(res, options, settings); } - - // Escape if necessary - if (m_escapeMethod != nullptr && options.contains("escape")) - { res = m_escapeMethod(res); } - - return res; -} - -QString Filename::fixSeparator(const QString &separator) const -{ - return QString(separator) - .replace("\\n", "\n") - .replace("\\r", "\r"); -} - -QString Filename::getFormat() const +QString Filename::format() const { return m_format; } @@ -550,8 +256,9 @@ void Filename::setEscapeMethod(QString (*escapeMethod)(const QVariant &)) bool Filename::returnError(const QString &msg, QString *error) const { - if (error != nullptr) + if (error != nullptr) { *error = msg; + } return false; } @@ -562,54 +269,62 @@ bool Filename::isValid(Profile *profile, QString *error) const static const QString green = QStringLiteral("%1"); // Field must be filled - if (m_format.isEmpty()) + if (m_format.isEmpty()) { return returnError(red.arg(QObject::tr("Filename must not be empty!")), error); + } // Can't validate javascript expressions - if (m_format.startsWith("javascript:")) - { + if (m_format.startsWith("javascript:")) { returnError(orange.arg(QObject::tr("Can't validate Javascript expressions.")), error); return true; } + const auto &toks = m_ast->tokens(); + // Field must end by an extension - if (!m_format.endsWith(".%ext%")) + if (!m_format.endsWith(".%ext%")) { return returnError(orange.arg(QObject::tr("Your filename doesn't ends by an extension, symbolized by %ext%! You may not be able to open saved files.")), error); + } // Field must contain an unique token - if (!m_format.contains("%md5%") && !m_format.contains("%id%") && !m_format.contains("%num%")) + if (!toks.contains("md5") && !toks.contains("id") && !toks.contains("num")) { return returnError(orange.arg(QObject::tr("Your filename is not unique to each image and an image may overwrite a previous one at saving! You should use%md5%, which is unique to each image, to avoid this inconvenience.")), error); + } // Looking for unknown tokens - QStringList tokens = QStringList() << "tags" << "artist" << "general" << "copyright" << "character" << "model" << "species" << "meta" << "filename" << "rating" << "md5" << "website" << "websitename" << "ext" << "all" << "id" << "search" << "search_(\\d+)" << "allo" << "date" << "score" << "count" << "width" << "height" << "pool" << "url_file" << "url_page" << "num"; - if (profile != nullptr) - { tokens.append(getCustoms(profile->getSettings()).keys()); } + QStringList tokens = QStringList() << "tags" << "artist" << "general" << "copyright" << "character" << "model" << "photo_set" << "species" << "meta" << "filename" << "rating" << "md5" << "website" << "websitename" << "ext" << "all" << "id" << "search" << "search_(\\d+)" << "allo" << "date" << "score" << "count" << "width" << "height" << "pool" << "url_file" << "url_page" << "num" << "name" << "position"; + if (profile != nullptr) { + tokens.append(profile->getAdditionalTokens()); + tokens.append(getCustoms(profile->getSettings()).keys()); + } static const QRegularExpression rx("%(.+?)%"); auto matches = rx.globalMatch(m_format); - while (matches.hasNext()) - { + while (matches.hasNext()) { auto match = matches.next(); bool found = false; - for (const QString &token : tokens) - { - if (QRegularExpression("%" + token + "(?::[^%]+)?%").match(match.captured(0)).hasMatch()) + for (const QString &token : tokens) { + if (QRegularExpression("%(?:gallery\\.)?" + token + "(?::[^%]+)?%").match(match.captured(0)).hasMatch()) { found = true; + } } - if (!found) + if (!found) { return returnError(orange.arg(QObject::tr("The %%1% token does not exist and will not be replaced.").arg(match.captured(1))), error); + } } // Check for invalid windows characters #ifdef Q_OS_WIN QString txt = QString(m_format).remove(rx); - if (txt.contains(':') || txt.contains('*') || txt.contains('?') || (txt.contains('"') && txt.count('<') == 0) || txt.count('<') != txt.count('>') || txt.contains('|')) + if (txt.contains(':') || txt.contains('*') || txt.contains('?') || (txt.contains('"') && txt.count('<') == 0) || txt.count('<') != txt.count('>') || txt.contains('|')) { return returnError(red.arg(QObject::tr("Your format contains characters forbidden on Windows! Forbidden characters: * ? \" : < > |")), error); + } #endif // Check if code is unique - if (!m_format.contains("%md5%") && !m_format.contains("%website%") && !m_format.contains("%websitename%") && !m_format.contains("%count%") && m_format.contains("%id%")) + if (!toks.contains("md5") && !toks.contains("website") && !toks.contains("websitename") && !toks.contains("count") && toks.contains("id")) { return returnError(green.arg(QObject::tr("You have chosen to use the %id% token. Know that it is only unique for a selected site. The same ID can identify different images depending on the site.")), error); + } // All tests passed returnError(green.arg(QObject::tr("Valid filename!")), error); @@ -618,9 +333,17 @@ bool Filename::isValid(Profile *profile, QString *error) const bool Filename::needTemporaryFile(const QMap &tokens) const { + if (m_format.startsWith("javascript:")) { + return false; + } + + const auto &toks = m_ast->tokens(); + return ( - (m_format.contains(QRegularExpression("%md5(?::([^%]+))?%")) && (!tokens.contains("md5") || tokens["md5"].value().toString().isEmpty())) || - (m_format.contains(QRegularExpression("%filesize(?::([^%]+))?%")) && (!tokens.contains("filesize") || tokens["filesize"].value().toInt() <= 0)) + (toks.contains("md5") && (!tokens.contains("md5") || tokens["md5"].value().toString().isEmpty())) || + (toks.contains("filesize") && (!tokens.contains("filesize") || tokens["filesize"].value().toInt() <= 0)) || + (toks.contains("width") && (!tokens.contains("width") || tokens["width"].value().toInt() <= 0)) || + (toks.contains("height") && (!tokens.contains("height") || tokens["height"].value().toInt() <= 0)) ); } @@ -632,34 +355,43 @@ int Filename::needExactTags(Site *site, const QString &api) const ? site->getApis().first()->forcedTokens() : QStringList(); - if (forcedTokens.contains("*")) + if (forcedTokens.contains("*")) { return 2; + } return needExactTags(forcedTokens); } int Filename::needExactTags(const QStringList &forcedTokens) const { // Javascript filenames always need tags as we don't know what they might do - if (m_format.startsWith("javascript:")) + if (m_format.startsWith("javascript:")) { return 2; + } + + const auto &toks = m_ast->tokens(); // If we need the filename and it is returned from the details page - if (m_format.contains(QRegularExpression("%filename(?::([^%]+))?%")) && forcedTokens.contains("filename")) + if (toks.contains("filename") && forcedTokens.contains("filename")) { return 2; + } // If we need the date and it is returned from the details page - if (m_format.contains(QRegularExpression("%date(?::([^%]+))?%")) && forcedTokens.contains("date")) + if (toks.contains("date") && forcedTokens.contains("date")) { return 2; + } // The filename contains one of the special tags - QStringList forbidden = QStringList() << "artist" << "copyright" << "character" << "model" << "species" << "meta" << "general"; - for (const QString &token : forbidden) - if (m_format.contains(QRegularExpression("%" + token + "(?::([^%]+))?%"))) + QStringList forbidden = QStringList() << "artist" << "copyright" << "character" << "model" << "photo_set" << "species" << "meta" << "general"; + for (const QString &token : forbidden) { + if (toks.contains(token)) { return 1; + } + } // Namespaces come from detailed tags - if (m_format.contains("includenamespace")) + if (m_format.contains("includenamespace")) { return 1; + } return 0; } diff --git a/lib/src/models/filename.h b/lib/src/models/filename.h index aaab63bd6..58c69f2ca 100644 --- a/lib/src/models/filename.h +++ b/lib/src/models/filename.h @@ -1,31 +1,50 @@ #ifndef FILENAME_H #define FILENAME_H +#include #include #include +#include #include #include #include -class Site; +class AstFilename; class Image; -class QJSEngine; -class QSettings; class Profile; +class QSettings; +class Site; class Token; class Filename { public: - Filename() = default; + enum PathFlag + { + None = 0, + ConditionalFilenames = 1, // complex (true) + ExpandConditionals = 2, // complex (true) + CapLength = 4, // maxLength (true) + Fix = 8, // shouldFixFilename (true) + IncludeFolder = 16, // getFull (false) + KeepInvalidTokens = 32, // keepInvalidTokens (false) + + Complex = ConditionalFilenames | ExpandConditionals, + File = CapLength | Fix, + Path = File | IncludeFolder, + Default = Complex | File, + }; + Q_DECLARE_FLAGS(PathFlags, PathFlag) + + Filename(); explicit Filename(QString format); - QString getFormat() const; + QString format() const; void setFormat(const QString &format); void setEscapeMethod(QString (*)(const QVariant &)); - QStringList path(const Image &img, Profile *profile, const QString &pth = "", int counter = 0, bool complex = true, bool maxLength = true, bool shouldFixFilename = true, bool getFull = false, bool keepInvalidTokens = false) const; - QStringList path(QMap tokens, Profile *profile, QString folder = "", int counter = 0, bool complex = true, bool maxLength = true, bool shouldFixFilename = true, bool getFull = false, bool keepInvalidTokens = false) const; + QStringList path(const Image &img, Profile *profile, const QString &pth = "", int counter = 0, PathFlags flags = Default) const; + QStringList path(QMap tokens, Profile *profile, QString folder = "", int counter = 0, PathFlags flags = Default) const; bool isValid(Profile *profile = nullptr, QString *error = nullptr) const; bool needTemporaryFile(const QMap &tokens) const; @@ -33,21 +52,18 @@ class Filename int needExactTags(Site *site, const QString &api = "") const; int needExactTags(const QStringList &forcedTokens = QStringList()) const; - QList> expandTokens(const QString &filename, QMap tokens, QSettings *settings) const; - QString expandConditionals(const QString &text, const QStringList &tags, const QMap &tokens, QSettings *settings, int depth = 0) const; + QList> expandTokens(QMap tokens, QSettings *settings) const; protected: - QString cleanUpValue(QString res, const QMap &options, QSettings *settings) const; - QString optionedValue(const QVariant &val, const QString &key, const QString &ops, QSettings *settings, QStringList namespaces) const; QList getReplace(const QString &key, const Token &token, QSettings *settings) const; bool returnError(const QString &msg, QString *error) const; - QString fixSeparator(const QString &separator) const; - void setJavaScriptVariables(QJSEngine &engine, QSettings *settings, const QMap &tokens) const; - bool matchConditionalFilename(QString cond, QSettings *settings, const QMap &tokens) const; private: QString m_format; + QSharedPointer m_ast; QString (*m_escapeMethod)(const QVariant &) = nullptr; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(Filename::PathFlags); + #endif // FILENAME_H diff --git a/lib/src/models/filtering/blacklist.cpp b/lib/src/models/filtering/blacklist.cpp index b858d9fb1..6a3b89845 100644 --- a/lib/src/models/filtering/blacklist.cpp +++ b/lib/src/models/filtering/blacklist.cpp @@ -6,17 +6,18 @@ Blacklist::Blacklist(const QStringList &tags) { - for (const QString &tag : tags) + for (const QString &tag : tags) { add(tag); + } } int Blacklist::indexOf(const QString &tag) const { - for (int i = 0; i < m_filters.count(); ++i) - { + for (int i = 0; i < m_filters.count(); ++i) { const auto &filters = m_filters[i]; - if (filters.count() == 1 && QString::compare(filters[0]->toString(), tag, Qt::CaseInsensitive) == 0) + if (filters.count() == 1 && QString::compare(filters[0]->toString(), tag, Qt::CaseInsensitive) == 0) { return i; + } } return -1; } @@ -29,29 +30,32 @@ bool Blacklist::contains(const QString &tag) const void Blacklist::add(const QString &tag) { auto filter = QSharedPointer(FilterFactory::build(tag)); - if (!filter.isNull()) + if (!filter.isNull()) { m_filters.append(QList>() << filter); + } } void Blacklist::add(const QStringList &tags) { QList> filters; - for (const QString &tag : tags) - { + for (const QString &tag : tags) { auto filter = QSharedPointer(FilterFactory::build(tag)); - if (!filter.isNull()) + if (!filter.isNull()) { filters.append(filter); + } } - if (!filters.isEmpty()) + if (!filters.isEmpty()) { m_filters.append(filters); + } } bool Blacklist::remove(const QString &tag) { const int index = indexOf(tag); - if (index == -1) + if (index == -1) { return false; + } m_filters.removeAt(index); return true; @@ -60,14 +64,14 @@ bool Blacklist::remove(const QString &tag) QString Blacklist::toString() const { QString ret; - for (const auto &filters : qAsConst(m_filters)) - { - if (!ret.isEmpty()) - { ret.append("\n"); } - for (int i = 0; i < filters.count(); ++i) - { - if (i != 0) - { ret.append(' '); } + for (const auto &filters : qAsConst(m_filters)) { + if (!ret.isEmpty()) { + ret.append("\n"); + } + for (int i = 0; i < filters.count(); ++i) { + if (i != 0) { + ret.append(' '); + } ret.append(filters[i]->toString()); } } @@ -77,21 +81,19 @@ QString Blacklist::toString() const QStringList Blacklist::match(const QMap &tokens, bool invert) const { QStringList detected; - for (const auto &filters : qAsConst(m_filters)) - { + for (const auto &filters : qAsConst(m_filters)) { bool allDetected = true; QStringList res; - for (const QSharedPointer &filter : filters) - { - if (filter->match(tokens, invert).isEmpty()) - { + for (const QSharedPointer &filter : filters) { + if (filter->match(tokens, invert).isEmpty()) { allDetected = false; break; } res.append(filter->toString()); } - if (allDetected) + if (allDetected) { detected.append(res.join(' ')); + } } return detected; } diff --git a/lib/src/models/filtering/filter-factory.cpp b/lib/src/models/filtering/filter-factory.cpp index 87546693c..4d06bff2a 100644 --- a/lib/src/models/filtering/filter-factory.cpp +++ b/lib/src/models/filtering/filter-factory.cpp @@ -9,23 +9,20 @@ Filter *FilterFactory::build(QString filter) bool invert = false; // Invert the filter by prepending '-' - if (filter.startsWith('-')) - { + if (filter.startsWith('-')) { filter = filter.right(filter.length() - 1); invert = true; } // Tokens - if (filter.startsWith('%') && filter.endsWith('%')) - { + if (filter.startsWith('%') && filter.endsWith('%')) { const QString token = filter.mid(1, filter.length() - 2); return new TokenFilter(token, invert); } // Meta-tags - if (filter.contains(":")) - { + if (filter.contains(":")) { const QString type = filter.section(':', 0, 0).toLower(); const QString val = filter.section(':', 1).toLower(); @@ -33,8 +30,7 @@ Filter *FilterFactory::build(QString filter) } // Tags - if (!filter.isEmpty()) - { + if (!filter.isEmpty()) { return new TagFilter(filter.trimmed(), invert); } diff --git a/lib/src/models/filtering/filter.h b/lib/src/models/filtering/filter.h index 0ef1da827..208198137 100644 --- a/lib/src/models/filtering/filter.h +++ b/lib/src/models/filtering/filter.h @@ -9,8 +9,10 @@ class Token; class Filter { - public: + protected: explicit Filter(bool invert = false); + + public: virtual ~Filter() = default; virtual QString match(const QMap &tokens, bool invert = false) const = 0; virtual QString toString() const = 0; diff --git a/lib/src/models/filtering/meta-filter.cpp b/lib/src/models/filtering/meta-filter.cpp index 7d74d77e4..14ce50c4e 100644 --- a/lib/src/models/filtering/meta-filter.cpp +++ b/lib/src/models/filtering/meta-filter.cpp @@ -19,8 +19,9 @@ QString MetaFilter::toString() const bool MetaFilter::compare(const Filter& rhs) const { const auto other = dynamic_cast(&rhs); - if (other == nullptr) + if (other == nullptr) { return false; + } return m_type == other->m_type && m_val == other->m_val; } @@ -28,11 +29,13 @@ bool MetaFilter::compare(const Filter& rhs) const static QDateTime stringToDate(const QString &text) { QDateTime date = QDateTime::fromString(text, "yyyy-MM-dd"); - if (date.isValid()) - { return date; } + if (date.isValid()) { + return date; + } date = QDateTime::fromString(text, "MM/dd/yyyy"); - if (date.isValid()) - { return date; } + if (date.isValid()) { + return date; + } return QDateTime(); } @@ -46,36 +49,43 @@ static QDateTime ageToDate(const QString &text) { static QRegularExpression rx("^(\\d+)(\\w+)$"); auto match = rx.match(text); - if (!match.hasMatch()) + if (!match.hasMatch()) { return QDateTime(); + } const int count = match.captured(1).toInt(); const QString type = match.captured(2); // Define "now" with the correct timezone QDateTime base; - if (ageToDateTestNow.isValid()) - { base = ageToDateTestNow; } - else - { + if (ageToDateTestNow.isValid()) { + base = ageToDateTestNow; + } else { base = QDateTime::currentDateTimeUtc(); base.setTimeZone(ageToDateImage.timeZone()); } - if (type.startsWith("y")) + if (type.startsWith("y")) { return base.addYears(-count); - if (type.startsWith("mo")) + } + if (type.startsWith("mo")) { return base.addMonths(-count); - if (type.startsWith("w")) + } + if (type.startsWith("w")) { return base.addDays(-(count * 7)); - if (type.startsWith("d")) + } + if (type.startsWith("d")) { return base.addDays(-count); - if (type.startsWith("h")) + } + if (type.startsWith("h")) { return base.addSecs(-(count * 60 * 60)); - if (type.startsWith("mi")) + } + if (type.startsWith("mi")) { return base.addSecs(-(count * 60)); - if (type.startsWith("s")) + } + if (type.startsWith("s")) { return base.addSecs(-count); + } return QDateTime(); } @@ -83,90 +93,99 @@ static QDateTime ageToDate(const QString &text) template static bool rangeCheck(T (*converter)(const QString &), T input, const QString &val) { - if (val.startsWith("..") || val.startsWith("<=")) - { return input <= converter(val.right(val.size() - 2)); } - if (val.endsWith("..")) - { return input >= converter(val.left(val.size() - 2)); } - if (val.startsWith(">=")) - { return input >= converter(val.right(val.size() - 2)); } - if (val.startsWith("<")) - { return input < converter(val.right(val.size() - 1)); } - if (val.startsWith(">")) - { return input > converter(val.right(val.size() - 1)); } - if (val.contains("..")) - { return input >= converter(val.left(val.indexOf(".."))) && input <= converter(val.right(val.size() - val.indexOf("..") - 2)); } + if (val.startsWith("..") || val.startsWith("<=")) { + return input <= converter(val.right(val.size() - 2)); + } + if (val.endsWith("..")) { + return input >= converter(val.left(val.size() - 2)); + } + if (val.startsWith(">=")) { + return input >= converter(val.right(val.size() - 2)); + } + if (val.startsWith("<")) { + return input < converter(val.right(val.size() - 1)); + } + if (val.startsWith(">")) { + return input > converter(val.right(val.size() - 1)); + } + if (val.contains("..")) { + return input >= converter(val.left(val.indexOf(".."))) && input <= converter(val.right(val.size() - val.indexOf("..") - 2)); + } return input == converter(val); } QString MetaFilter::match(const QMap &tokens, bool invert) const { - if (m_invert) - { invert = !invert; } + if (m_invert) { + invert = !invert; + } // Grabber specials - if (m_type == QStringLiteral("grabber")) - { + if (m_type == QStringLiteral("grabber")) { const QStringList &vals = tokens[m_type].value().toStringList(); const bool cond = vals.contains(m_val, Qt::CaseInsensitive); - if (!cond && !invert) - { return QObject::tr("image is not \"%1\"").arg(m_val); } - if (cond && invert) - { return QObject::tr("image is \"%1\"").arg(m_val); } + if (!cond && !invert) { + return QObject::tr("image is not \"%1\"").arg(m_val); + } + if (cond && invert) { + return QObject::tr("image is \"%1\"").arg(m_val); + } return QString(); } // Non-token metas - if (m_type == "age") - { - if (!tokens.contains("date")) - { return QObject::tr("An image needs a date to be filtered by age"); } + if (m_type == "age") { + if (!tokens.contains("date")) { + return QObject::tr("An image needs a date to be filtered by age"); + } const QDateTime &date = tokens["date"].value().toDateTime(); ageToDateImage = date; ageToDateTestNow = tokens["TESTS_now"].value().toDateTime(); const bool cond = rangeCheck(ageToDate, date, m_val); - if (cond && !invert) - { return QObject::tr("image's %1 does not match").arg(m_type); } - if (!cond && invert) - { return QObject::tr("image's %1 match").arg(m_type); } + if (cond && !invert) { + return QObject::tr("image's %1 does not match").arg(m_type); + } + if (!cond && invert) { + return QObject::tr("image's %1 match").arg(m_type); + } return QString(); } // Meta tokens - if (!tokens.contains(m_type)) - { + if (!tokens.contains(m_type)) { QStringList keys = tokens.keys(); return QObject::tr(R"(unknown type "%1" (available types: "%2"))").arg(m_type, keys.join("\", \"")); } const QVariant &token = tokens[m_type].value(); - if (token.type() == QVariant::Int || token.type() == QVariant::DateTime || token.type() == QVariant::ULongLong) - { + if (token.type() == QVariant::Int || token.type() == QVariant::DateTime || token.type() == QVariant::ULongLong) { int input = 0; - if (token.type() == QVariant::Int) - { input = token.toInt(); } - else if (token.type() == QVariant::ULongLong) - { input = token.toULongLong(); } + if (token.type() == QVariant::Int) { + input = token.toInt(); + } else if (token.type() == QVariant::ULongLong) { + input = token.toULongLong(); + } bool cond; - if (token.type() == QVariant::DateTime) - { cond = rangeCheck(stringToDate, token.toDateTime(), m_val); } - else - { cond = rangeCheck(stringToInt, input, m_val); } - - if (!cond && !invert) - { return QObject::tr("image's %1 does not match").arg(m_type); } - if (cond && invert) - { return QObject::tr("image's %1 match").arg(m_type); } - } - else - { - if (m_type == "rating") - { + if (token.type() == QVariant::DateTime) { + cond = rangeCheck(stringToDate, token.toDateTime(), m_val); + } else { + cond = rangeCheck(stringToInt, input, m_val); + } + + if (!cond && !invert) { + return QObject::tr("image's %1 does not match").arg(m_type); + } + if (cond && invert) { + return QObject::tr("image's %1 match").arg(m_type); + } + } else { + if (m_type == "rating") { QMap assoc; assoc["s"] = "safe"; assoc["q"] = "questionable"; @@ -175,30 +194,32 @@ QString MetaFilter::match(const QMap &tokens, bool invert) const const QString val = assoc.contains(m_val) ? assoc[m_val] : m_val; const bool cond = !val.isEmpty() && token.toString().toLower().startsWith(val.at(0)); - if (!cond && !invert) - { return QObject::tr("image is not \"%1\"").arg(val); } - if (cond && invert) - { return QObject::tr("image is \"%1\"").arg(val); } - } - else if (m_type == "source") - { + if (!cond && !invert) { + return QObject::tr("image is not \"%1\"").arg(val); + } + if (cond && invert) { + return QObject::tr("image is \"%1\"").arg(val); + } + } else if (m_type == "source") { QRegExp rx(m_val + "*", Qt::CaseInsensitive, QRegExp::Wildcard); const bool cond = rx.exactMatch(token.toString()); - if (!cond && !invert) - { return QObject::tr("image's source does not starts with \"%1\"").arg(m_val); } - if (cond && invert) - { return QObject::tr("image's source starts with \"%1\"").arg(m_val); } - } - else - { + if (!cond && !invert) { + return QObject::tr("image's source does not starts with \"%1\"").arg(m_val); + } + if (cond && invert) { + return QObject::tr("image's source starts with \"%1\"").arg(m_val); + } + } else { const QString input = token.toString(); const bool cond = input == m_val; - if (!cond && !invert) - { return QObject::tr("image's %1 does not match").arg(m_type); } - if (cond && invert) - { return QObject::tr("image's %1 match").arg(m_type); } + if (!cond && !invert) { + return QObject::tr("image's %1 does not match").arg(m_type); + } + if (cond && invert) { + return QObject::tr("image's %1 match").arg(m_type); + } } } diff --git a/lib/src/models/filtering/post-filter.cpp b/lib/src/models/filtering/post-filter.cpp index 1f24a0686..3a62246b2 100644 --- a/lib/src/models/filtering/post-filter.cpp +++ b/lib/src/models/filtering/post-filter.cpp @@ -5,11 +5,11 @@ PostFilter::PostFilter(const QStringList &filters) { - for (const QString &filter : filters) - { + for (const QString &filter : filters) { auto fil = QSharedPointer(FilterFactory::build(filter)); - if (!fil.isNull()) + if (!fil.isNull()) { m_filters.append(fil); + } } } @@ -21,11 +21,11 @@ int PostFilter::count() const QStringList PostFilter::match(const QMap &tokens) const { QStringList ret; - for (const auto &filter : m_filters) - { + for (const auto &filter : m_filters) { QString err = filter->match(tokens); - if (!err.isEmpty()) + if (!err.isEmpty()) { ret.append(err); + } } return ret; } diff --git a/lib/src/models/filtering/tag-filter.cpp b/lib/src/models/filtering/tag-filter.cpp index 092214b55..9f5387d6f 100644 --- a/lib/src/models/filtering/tag-filter.cpp +++ b/lib/src/models/filtering/tag-filter.cpp @@ -7,8 +7,9 @@ TagFilter::TagFilter(QString tag, bool invert) : Filter(invert), m_tag(std::move(tag)) { - if (m_tag.contains('*')) - { m_regexp.reset(new QRegExp(m_tag, Qt::CaseInsensitive, QRegExp::Wildcard)); } + if (m_tag.contains('*')) { + m_regexp.reset(new QRegExp(m_tag, Qt::CaseInsensitive, QRegExp::Wildcard)); + } } QString TagFilter::toString() const @@ -19,35 +20,37 @@ QString TagFilter::toString() const bool TagFilter::compare(const Filter& rhs) const { const auto other = dynamic_cast(&rhs); - if (other == nullptr) + if (other == nullptr) { return false; + } return m_tag == other->m_tag; } QString TagFilter::match(const QMap &tokens, bool invert) const { - if (m_invert) - { invert = !invert; } + if (m_invert) { + invert = !invert; + } const QStringList &tags = tokens["allos"].value().toStringList(); // Check if any tag match the filter (case insensitive plain text with wildcards allowed) bool cond = false; - for (const QString &tag : tags) - { + for (const QString &tag : tags) { const bool match = m_regexp.isNull() ? tag == m_tag : m_regexp->exactMatch(tag); - if (match) - { + if (match) { cond = true; break; } } - if (!cond && !invert) - { return QObject::tr("image does not contains \"%1\"").arg(m_tag); } - if (cond && invert) - { return QObject::tr("image contains \"%1\"").arg(m_tag); } + if (!cond && !invert) { + return QObject::tr("image does not contains \"%1\"").arg(m_tag); + } + if (cond && invert) { + return QObject::tr("image contains \"%1\"").arg(m_tag); + } return QString(); } diff --git a/lib/src/models/filtering/token-filter.cpp b/lib/src/models/filtering/token-filter.cpp index e16a236f2..07f2bc12c 100644 --- a/lib/src/models/filtering/token-filter.cpp +++ b/lib/src/models/filtering/token-filter.cpp @@ -16,23 +16,27 @@ QString TokenFilter::toString() const bool TokenFilter::compare(const Filter &rhs) const { const auto other = dynamic_cast(&rhs); - if (other == nullptr) + if (other == nullptr) { return false; + } return m_token == other->m_token; } QString TokenFilter::match(const QMap &tokens, bool invert) const { - if (m_invert) - { invert = !invert; } + if (m_invert) { + invert = !invert; + } const bool cond = tokens.contains(m_token) && !isVariantEmpty(tokens[m_token].value()); - if (cond && invert) - { return QObject::tr("image has a \"%1\" token").arg(m_token); } - if (!cond && !invert) - { return QObject::tr("image does not have a \"%1\" token").arg(m_token); } + if (cond && invert) { + return QObject::tr("image has a \"%1\" token").arg(m_token); + } + if (!cond && !invert) { + return QObject::tr("image does not have a \"%1\" token").arg(m_token); + } return QString(); } diff --git a/lib/src/models/image-size.cpp b/lib/src/models/image-size.cpp new file mode 100644 index 000000000..03d642dac --- /dev/null +++ b/lib/src/models/image-size.cpp @@ -0,0 +1,141 @@ +#include "image-size.h" +#include +#include +#include + + +ImageSize::~ImageSize() +{ + if (!m_temporaryPath.isEmpty()) { + QFile::remove(m_temporaryPath); + } +} + + +QString ImageSize::savePath() const +{ + return m_savePath; +} + + +QString ImageSize::save(const QString &path) +{ + // If we have a temporary path for this image, we move it to the destination + if (!m_temporaryPath.isEmpty() && QFile::exists(m_temporaryPath)) { + QString temp = m_temporaryPath; + QFile(m_temporaryPath).rename(path); + + m_temporaryPath.clear(); + m_savePath = path; + + return temp; + } + + // If we already saved this image somewhere, simply make a copy of this file + if (!m_savePath.isEmpty() && QFile::exists(m_savePath)) { + QFile(m_savePath).copy(path); + return m_savePath; + } + + return QString(); +} + +bool ImageSize::setTemporaryPath(const QString &path) +{ + bool changed = setSavePath(path); + + if (m_temporaryPath == path) { + return changed; + } + + if (!m_temporaryPath.isEmpty()) { + QFile::remove(m_temporaryPath); + } + + m_temporaryPath = path; + + if (fileSize <= 0) { + fileSize = QFileInfo(m_temporaryPath).size(); + return true; + } + + return changed; +} + +bool ImageSize::setSavePath(const QString &path) +{ + if (path != m_savePath) { + m_savePath = path; + + if (fileSize <= 0) { + fileSize = QFileInfo(m_savePath).size(); + } + + return true; + } + + return false; +} + + +QPixmap ImageSize::pixmap() const +{ + return m_pixmap; +} + +const QPixmap &ImageSize::pixmap() +{ + return m_pixmap; +} + +void ImageSize::setPixmap(const QPixmap &pixmap) +{ + m_pixmap = !rect.isNull() + ? pixmap.copy(rect) + : pixmap; +} + + +void ImageSize::read(const QJsonObject &json) +{ + fileSize = json["fileSize"].toInt(); + + if (json.contains("size") && json["size"].isObject()) { + QJsonObject sz = json["size"].toObject(); + size = QSize( + sz["width"].toInt(), + sz["height"].toInt() + ); + } + + if (json.contains("rect") && json["rect"].isObject()) { + QJsonObject rct = json["rect"].toObject(); + rect = QRect( + rct["left"].toInt(), + rct["top"].toInt(), + rct["width"].toInt(), + rct["height"].toInt() + ); + } +} + +void ImageSize::write(QJsonObject &json) const +{ + json["fileSize"] = fileSize; + + if (size.isValid()) { + QJsonObject sz; + sz["width"] = size.width(); + sz["height"] = size.height(); + json["size"] = sz; + } + + if (rect.isValid()) { + QJsonObject rct; + rct["left"] = rect.left(); + rct["top"] = rect.top(); + rct["width"] = rect.width(); + rct["height"] = rect.height(); + json["rect"] = rct; + } +} diff --git a/lib/src/models/image-size.h b/lib/src/models/image-size.h new file mode 100644 index 000000000..69a155b4f --- /dev/null +++ b/lib/src/models/image-size.h @@ -0,0 +1,41 @@ +#ifndef IMAGE_SIZE_H +#define IMAGE_SIZE_H + +#include +#include +#include +#include + + +class QJsonObject; + +struct ImageSize +{ + ~ImageSize(); + + QSize size; + qint64 fileSize = 0; + QRect rect; + + // Filesystem cache + QString save(const QString &path); + QString savePath() const; + bool setTemporaryPath(const QString &path); + bool setSavePath(const QString &path); + + // Pixmap cache + QPixmap pixmap() const; + const QPixmap &pixmap(); + void setPixmap(const QPixmap &pixmap); + + // Serialization + void read(const QJsonObject &json); + void write(QJsonObject &json) const; + + private: + QString m_temporaryPath; + QString m_savePath; + QPixmap m_pixmap; +}; + +#endif // IMAGE_SIZE_H diff --git a/lib/src/models/image.cpp b/lib/src/models/image.cpp index 8f204404d..3bddc3d34 100644 --- a/lib/src/models/image.cpp +++ b/lib/src/models/image.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include "favorite.h" #include "functions.h" #include "loader/token.h" +#include "logger.h" #include "models/api/api.h" #include "models/filename.h" #include "models/image.h" @@ -25,24 +27,9 @@ #define MAX_LOAD_FILESIZE (1024 * 1024 * 50) -QUrl removeCacheUrl(QUrl url) -{ - const QString query = url.query(); - if (query.isEmpty()) - return url; - - // Only remove ?integer - bool ok; - query.toInt(&ok); - if (ok) - url.setQuery(QString()); - - return url; -} - Image::Image() : m_profile(nullptr), m_extensionRotator(nullptr) -{ } +{} // TODO(Bionus): clean up this mess Image::Image(const Image &other) @@ -54,7 +41,6 @@ Image::Image(const Image &other) m_id = other.m_id; m_score = other.m_score; m_parentId = other.m_parentId; - m_fileSize = other.m_fileSize; m_authorId = other.m_authorId; m_hasChildren = other.m_hasChildren; @@ -69,25 +55,22 @@ Image::Image(const Image &other) m_status = other.m_status; m_rating = other.m_rating; m_sources = other.m_sources; - m_savePath = other.m_savePath; m_pageUrl = other.m_pageUrl; m_fileUrl = other.m_fileUrl; m_sampleUrl = other.m_sampleUrl; m_previewUrl = other.m_previewUrl; - m_size = other.m_size; - m_imagePreview = other.m_imagePreview; + m_sizes = other.m_sizes; m_createdAt = other.m_createdAt; - m_data = other.m_data; m_galleryCount = other.m_galleryCount; + m_position = other.m_position; m_loadDetails = other.m_loadDetails; m_tags = other.m_tags; m_pools = other.m_pools; - m_timer = other.m_timer; m_profile = other.m_profile; m_settings = other.m_settings; m_search = other.m_search; @@ -98,13 +81,16 @@ Image::Image(const Image &other) } Image::Image(Site *site, QMap details, Profile *profile, Page *parent) - : m_profile(profile), m_id(0), m_parentSite(site), m_extensionRotator(nullptr) + : Image(site, details, QVariantMap(), profile, parent) +{} + +Image::Image(Site *site, QMap details, QVariantMap data, Profile *profile, Page *parent) + : m_profile(profile), m_id(0), m_parentSite(site), m_extensionRotator(nullptr), m_data(std::move(data)) { m_settings = m_profile->getSettings(); // Parents - if (m_parentSite == nullptr) - { + if (m_parentSite == nullptr) { log(QStringLiteral("Image has nullptr parent, aborting creation.")); return; } @@ -120,7 +106,6 @@ Image::Image(Site *site, QMap details, Profile *profile, Page m_score = details.contains("score") ? details["score"].toInt() : 0; m_hasScore = details.contains("score"); m_parentId = details.contains("parent_id") ? details["parent_id"].toInt() : 0; - m_fileSize = details.contains("file_size") ? details["file_size"].toInt() : 0; m_authorId = details.contains("creator_id") ? details["creator_id"].toInt() : 0; m_hasChildren = details.contains("has_children") && details["has_children"] == "true"; m_hasNote = details.contains("has_note") && details["has_note"] == "true"; @@ -128,25 +113,47 @@ Image::Image(Site *site, QMap details, Profile *profile, Page m_fileUrl = details.contains("file_url") ? m_parentSite->fixUrl(details["file_url"]) : QUrl(); m_sampleUrl = details.contains("sample_url") ? m_parentSite->fixUrl(details["sample_url"]) : QUrl(); m_previewUrl = details.contains("preview_url") ? m_parentSite->fixUrl(details["preview_url"]) : QUrl(); - m_size = QSize(details.contains("width") ? details["width"].toInt() : 0, details.contains("height") ? details["height"].toInt() : 0); m_sources = details.contains("sources") ? details["sources"].split('\n') : (details.contains("source") ? QStringList() << details["source"] : QStringList()); m_galleryCount = details.contains("gallery_count") ? details["gallery_count"].toInt() : -1; + m_position = details.contains("position") ? details["position"].toInt() : 0; - // Preview rect - if (details.contains("preview_rect")) + // Sizes + static QMap prefixes = { - const QStringList rect = details["preview_rect"].split(';'); - m_previewRect = QRect(rect[0].toInt(), rect[1].toInt(), rect[2].toInt(), rect[3].toInt()); + { Image::Size::Full, "" }, + { Image::Size::Sample, "sample_" }, + { Image::Size::Thumbnail, "preview_" }, + }; + for (auto it = prefixes.constBegin(); it != prefixes.constEnd(); ++it) { + const QString &prefix = it.value(); + + auto is = QSharedPointer::create(); + + is->size = details.contains(prefix + "width") && details.contains(prefix + "height") + ? QSize(details[prefix + "width"].toInt(), details[prefix + "height"].toInt()) + : QSize(); + is->fileSize = details.contains(prefix + "file_size") ? details[prefix + "file_size"].toInt() : 0; + + if (details.contains(prefix + "rect")) { + const QStringList rect = details[prefix + "rect"].split(';'); + if (rect.count() == 4) { + is->rect = QRect(rect[0].toInt(), rect[1].toInt(), rect[2].toInt(), rect[3].toInt()); + } else { + log("Invalid number of values for image rectangle", Logger::Error); + } + } + + m_sizes.insert(it.key(), is); } // Page url - if (details.contains("page_url")) - { m_pageUrl = details["page_url"]; } - else - { + if (details.contains("page_url")) { + m_pageUrl = details["page_url"]; + } else { Api *api = m_parentSite->detailsApi(); - if (api != nullptr) - { m_pageUrl = api->detailsUrl(m_id, m_md5, m_parentSite).url; } + if (api != nullptr) { + m_pageUrl = api->detailsUrl(m_id, m_md5, m_parentSite).url; + } } m_pageUrl = site->fixUrl(m_pageUrl).toString(); @@ -155,22 +162,20 @@ Image::Image(Site *site, QMap details, Profile *profile, Page // Tags QStringList types = QStringList() << "general" << "artist" << "character" << "copyright" << "model" << "species" << "meta"; - for (const QString &typ : types) - { + for (const QString &typ : types) { const QString key = "tags_" + typ; - if (!details.contains(key)) + if (!details.contains(key)) { continue; + } const TagType ttype(typ); QStringList t = details[key].split(' ', QString::SkipEmptyParts); - for (QString tg : t) - { + for (QString tg : t) { tg.replace("&", "&"); m_tags.append(Tag(tg, ttype)); } } - if (m_tags.isEmpty() && details.contains("tags")) - { + if (m_tags.isEmpty() && details.contains("tags")) { QString tgs = QString(details["tags"]).replace(QRegularExpression("[\r\n\t]+"), " "); // Automatically find tag separator and split the list @@ -180,104 +185,104 @@ Image::Image(Site *site, QMap details, Profile *profile, Page ? tgs.split(", ", QString::SkipEmptyParts) : tgs.split(" ", QString::SkipEmptyParts); - for (QString tg : t) - { + for (QString tg : t) { tg.replace("&", "&"); const int colon = tg.indexOf(':'); - if (colon != -1) - { + if (colon != -1) { const QString tp = tg.left(colon).toLower(); - if (tp == "user") - { m_author = tg.mid(colon + 1); } - else if (tp == "score") - { m_score = tg.midRef(colon + 1).toInt(); } - else if (tp == "size") - { + if (tp == "user") { + m_author = tg.mid(colon + 1); + } else if (tp == "score") { + m_score = tg.midRef(colon + 1).toInt(); + } else if (tp == "size") { QStringList size = tg.mid(colon + 1).split('x'); - if (size.size() == 2) - m_size = QSize(size[0].toInt(), size[1].toInt()); + if (size.size() == 2) { + setSize(QSize(size[0].toInt(), size[1].toInt()), Size::Full); + } + } else if (tp == "rating") { + setRating(tg.mid(colon + 1)); + } else { + m_tags.append(Tag(tg)); } - else if (tp == "rating") - { setRating(tg.mid(colon + 1)); } - else - { m_tags.append(Tag(tg)); } + } else { + m_tags.append(Tag(tg)); } - else - { m_tags.append(Tag(tg)); } } } // Complete missing tag type information m_parentSite->tagDatabase()->load(); QStringList unknownTags; - for (const Tag &tag : qAsConst(m_tags)) - if (tag.type().isUnknown()) + for (const Tag &tag : qAsConst(m_tags)) { + if (tag.type().isUnknown()) { unknownTags.append(tag.text()); + } + } QMap dbTypes = m_parentSite->tagDatabase()->getTagTypes(unknownTags); - for (Tag &tag : m_tags) - if (dbTypes.contains(tag.text())) + for (Tag &tag : m_tags) { + if (dbTypes.contains(tag.text())) { tag.setType(dbTypes[tag.text()]); + } + } // Get file url and try to improve it to save bandwidth m_url = m_fileUrl; const QString ext = getExtension(m_url); - if (details.contains("ext") && !details["ext"].isEmpty()) - { + if (details.contains("ext") && !details["ext"].isEmpty()) { const QString realExt = details["ext"]; - if (ext != realExt) - { setFileExtension(realExt); } - } - else if (ext == QLatin1String("jpg") && !m_previewUrl.isEmpty()) - { + if (ext != realExt) { + setFileExtension(realExt); + } + } else if (ext == QLatin1String("jpg") && !m_previewUrl.isEmpty()) { bool fixed = false; const QString previewExt = getExtension(QUrl(details["preview_url"])); - if (!m_sampleUrl.isEmpty()) - { + if (!m_sampleUrl.isEmpty()) { // Guess extension from sample url const QString sampleExt = getExtension(QUrl(details["sample_url"])); - if (sampleExt != QLatin1String("jpg") && sampleExt != QLatin1String("png") && sampleExt != ext && previewExt == ext) - { + if (sampleExt != QLatin1String("jpg") && sampleExt != QLatin1String("png") && sampleExt != ext && previewExt == ext) { m_url = setExtension(m_url, sampleExt); fixed = true; } } // Guess the extension from the tags - if (!fixed) - { - if ((hasTag(QStringLiteral("swf")) || hasTag(QStringLiteral("flash"))) && ext != QLatin1String("swf")) - { setFileExtension(QStringLiteral("swf")); } - else if ((hasTag(QStringLiteral("gif")) || hasTag(QStringLiteral("animated_gif"))) && ext != QLatin1String("webm") && ext != QLatin1String("mp4")) - { setFileExtension(QStringLiteral("gif")); } - else if (hasTag(QStringLiteral("mp4")) && ext != QLatin1String("gif") && ext != QLatin1String("webm")) - { setFileExtension(QStringLiteral("mp4")); } - else if (hasTag(QStringLiteral("animated_png")) && ext != QLatin1String("webm") && ext != QLatin1String("mp4")) - { setFileExtension(QStringLiteral("png")); } - else if ((hasTag(QStringLiteral("webm")) || hasTag(QStringLiteral("animated"))) && ext != QLatin1String("gif") && ext != QLatin1String("mp4")) - { setFileExtension(QStringLiteral("webm")); } + if (!fixed) { + if ((hasTag(QStringLiteral("swf")) || hasTag(QStringLiteral("flash"))) && ext != QLatin1String("swf")) { + setFileExtension(QStringLiteral("swf")); + } else if ((hasTag(QStringLiteral("gif")) || hasTag(QStringLiteral("animated_gif"))) && ext != QLatin1String("webm") && ext != QLatin1String("mp4")) { + setFileExtension(QStringLiteral("gif")); + } else if (hasTag(QStringLiteral("mp4")) && ext != QLatin1String("gif") && ext != QLatin1String("webm")) { + setFileExtension(QStringLiteral("mp4")); + } else if (hasTag(QStringLiteral("animated_png")) && ext != QLatin1String("webm") && ext != QLatin1String("mp4")) { + setFileExtension(QStringLiteral("png")); + } else if ((hasTag(QStringLiteral("webm")) || hasTag(QStringLiteral("animated"))) && ext != QLatin1String("gif") && ext != QLatin1String("mp4")) { + setFileExtension(QStringLiteral("webm")); + } } + } else if (details.contains("image") && details["image"].contains("MB // gif\" height=\"") && ext != QLatin1String("gif")) { + m_url = setExtension(m_url, QStringLiteral("gif")); } - else if (details.contains("image") && details["image"].contains("MB // gif\" height=\"") && ext != QLatin1String("gif")) - { m_url = setExtension(m_url, QStringLiteral("gif")); } // Remove ? in urls - m_url = removeCacheUrl(m_url); - m_fileUrl = removeCacheUrl(m_fileUrl); - m_sampleUrl = removeCacheUrl(m_sampleUrl); - m_previewUrl = removeCacheUrl(m_previewUrl); + m_url = removeCacheBuster(m_url); + m_fileUrl = removeCacheBuster(m_fileUrl); + m_sampleUrl = removeCacheBuster(m_sampleUrl); + m_previewUrl = removeCacheBuster(m_previewUrl); // We use the sample URL as the URL for zip files (ugoira) or if the setting is set const bool downloadOriginals = m_settings->value("Save/downloadoriginals", true).toBool(); - if (!m_sampleUrl.isEmpty() && (getExtension(m_url) == "zip" || !downloadOriginals)) + if (!m_sampleUrl.isEmpty() && (getExtension(m_url) == "zip" || !downloadOriginals)) { m_url = m_sampleUrl.toString(); + } // Creation date m_createdAt = QDateTime(); - if (details.contains("created_at")) - { m_createdAt = qDateTimeFromString(details["created_at"]); } - else if (details.contains("date")) - { m_createdAt = QDateTime::fromString(details["date"], Qt::ISODate); } + if (details.contains("created_at")) { + m_createdAt = qDateTimeFromString(details["created_at"]); + } else if (details.contains("date")) { + m_createdAt = QDateTime::fromString(details["date"], Qt::ISODate); + } // Setup extension rotator const bool animated = hasTag("gif") || hasTag("animated_gif") || hasTag("mp4") || hasTag("animated_png") || hasTag("webm") || hasTag("animated"); @@ -294,36 +299,111 @@ Image::Image(Site *site, QMap details, Profile *profile, Page m_pools = QList(); } -Image::~Image() + +void Image::write(QJsonObject &json) const { - if (!m_temporaryPath.isEmpty()) - QFile::remove(m_temporaryPath); + json["website"] = m_parentSite->url(); + + if (!m_parentGallery.isNull()) { + QJsonObject jsonGallery; + m_parentGallery->write(jsonGallery); + json["gallery"] = jsonGallery; + } + + QStringList tags; + tags.reserve(m_tags.count()); + for (const Tag &tag : m_tags) { + tags.append(tag.text()); + } + + // FIXME: real serialization + json["name"] = m_name; + json["id"] = static_cast(m_id); + json["md5"] = m_md5; + json["rating"] = m_rating; + json["tags"] = QJsonArray::fromStringList(tags); + json["file_url"] = m_fileUrl.toString(); + json["date"] = m_createdAt.toString(Qt::ISODate); + json["search"] = QJsonArray::fromStringList(m_search); +} + +bool Image::read(const QJsonObject &json, const QMap &sites) +{ + const QString site = json["website"].toString(); + if (!sites.contains(site)) { + return false; + } + + if (json.contains("gallery")) { + auto gallery = new Image(); + if (gallery->read(json["gallery"].toObject(), sites)) { + m_parentGallery = QSharedPointer(gallery); + } else { + gallery->deleteLater(); + return false; + } + } + + m_parentSite = sites[site]; + using ::savePath; + m_profile = new Profile(savePath()); // FIXME + + m_name = json["name"].toString(); + m_id = json["id"].toInt(); + m_md5 = json["md5"].toString(); + m_rating = json["rating"].toString(); + m_fileUrl = json["file_url"].toString(); + m_createdAt = QDateTime::fromString(json["date"].toString(), Qt::ISODate); + + // Tags + QJsonArray jsonTags = json["tags"].toArray(); + m_tags.reserve(jsonTags.count()); + for (const auto &tag : jsonTags) { + m_tags.append(Tag(tag.toString())); + } + + // Search + QJsonArray jsonSearch = json["search"].toArray(); + m_search.reserve(jsonSearch.count()); + for (const auto &tag : jsonSearch) { + m_search.append(tag.toString()); + } + + m_sizes = { + { Image::Size::Full, QSharedPointer::create() }, + { Image::Size::Sample, QSharedPointer::create() }, + { Image::Size::Thumbnail, QSharedPointer::create() }, + }; + + return true; } + void Image::loadDetails(bool rateLimit) { - if (m_loadingDetails) + if (m_loadingDetails) { return; + } - if (m_loadedDetails || m_pageUrl.isEmpty()) - { + if (m_loadedDetails || m_pageUrl.isEmpty()) { emit finishedLoadingTags(); return; } // Load the request with a possible delay const int ms = m_parentSite->msToRequest(rateLimit ? Site::QueryType::Retry : Site::QueryType::List); - if (ms > 0) - { QTimer::singleShot(ms, this, SLOT(loadDetailsNow())); } - else - { loadDetailsNow(); } + if (ms > 0) { + QTimer::singleShot(ms, this, SLOT(loadDetailsNow())); + } else { + loadDetailsNow(); + } } void Image::loadDetailsNow() { - if (m_loadDetails != nullptr) - { - if (m_loadDetails->isRunning()) + if (m_loadDetails != nullptr) { + if (m_loadDetails->isRunning()) { m_loadDetails->abort(); + } m_loadDetails->deleteLater(); } @@ -336,8 +416,7 @@ void Image::loadDetailsNow() } void Image::abortTags() { - if (m_loadingDetails && m_loadDetails->isRunning()) - { + if (m_loadingDetails && m_loadDetails->isRunning()) { m_loadDetails->abort(); m_loadingDetails = false; } @@ -346,88 +425,91 @@ void Image::parseDetails() { m_loadingDetails = false; - // Aborted - if (m_loadDetails->error() == QNetworkReply::OperationCanceledError) - { - m_loadDetails->deleteLater(); - m_loadDetails = nullptr; - return; - } - // Check redirection QUrl redir = m_loadDetails->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redir.isEmpty()) - { + if (!redir.isEmpty()) { m_pageUrl = redir; loadDetails(); return; } const int statusCode = m_loadDetails->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 429) - { + if (statusCode == 429) { log(QStringLiteral("Details limit reached (429). New try.")); loadDetails(true); return; } + // Aborted or connection error + if (m_loadDetails->error()) { + if (m_loadDetails->error() != QNetworkReply::OperationCanceledError) { + log(QStringLiteral("Loading error for '%1': %2").arg(m_pageUrl.toString(), m_loadDetails->errorString()), Logger::Error); + } + m_loadDetails->deleteLater(); + m_loadDetails = nullptr; + return; + } + const QString source = QString::fromUtf8(m_loadDetails->readAll()); // Get an api able to parse details Api *api = m_parentSite->detailsApi(); - if (api == nullptr) + if (api == nullptr) { return; + } // Parse source - ParsedDetails ret = api->parseDetails(source, m_parentSite); - if (!ret.error.isEmpty()) - { - log(QStringLiteral("[%1][%2] %3").arg(m_parentSite->url(), api->getName(), ret.error), Logger::Warning); + ParsedDetails ret = api->parseDetails(source, statusCode, m_parentSite); + if (!ret.error.isEmpty()) { + auto logLevel = m_detailsParsWarnAsErr ? Logger::Error : Logger::Warning; + log(QStringLiteral("[%1][%2] %3").arg(m_parentSite->url(), api->getName(), ret.error), logLevel); emit finishedLoadingTags(); return; } // Fill data from parsing result - if (!ret.pools.isEmpty()) - { m_pools = ret.pools; } - if (!ret.tags.isEmpty()) - { m_tags = ret.tags; } - if (ret.createdAt.isValid()) - { m_createdAt = ret.createdAt; } + if (!ret.pools.isEmpty()) { + m_pools = ret.pools; + } + if (!ret.tags.isEmpty()) { + m_tags = ret.tags; + } + if (ret.createdAt.isValid()) { + m_createdAt = ret.createdAt; + } // Image url - if (!ret.imageUrl.isEmpty()) - { + if (!ret.imageUrl.isEmpty()) { const QUrl before = m_url; const QUrl newUrl = m_parentSite->fixUrl(ret.imageUrl, before); m_url = newUrl; m_fileUrl = newUrl; - if (before != m_url) - { - delete m_extensionRotator; - m_extensionRotator = nullptr; - setFileSize(0); + delete m_extensionRotator; + m_extensionRotator = nullptr; + + if (before != m_url) { + if (getExtension(before) != getExtension(m_url)) { + setFileSize(0, Size::Full); + } emit urlChanged(before, m_url); } } // Get rating from tags - if (m_rating.isEmpty()) - { + if (m_rating.isEmpty()) { int ratingTagIndex = -1; - for (int it = 0; it < m_tags.count(); ++it) - { - if (m_tags[it].type().name() == "rating") - { + for (int it = 0; it < m_tags.count(); ++it) { + if (m_tags[it].type().name() == "rating") { m_rating = m_tags[it].text(); ratingTagIndex = it; break; } } - if (ratingTagIndex != -1) - { m_tags.removeAt(ratingTagIndex); } + if (ratingTagIndex != -1) { + m_tags.removeAt(ratingTagIndex); + } } m_loadDetails->deleteLater(); @@ -439,107 +521,75 @@ void Image::parseDetails() emit finishedLoadingTags(); } -QStringList Image::path(QString fn, QString pth, int counter, bool complex, bool maxLength, bool shouldFixFilename, bool getFull) const -{ - Filename filename(fn); - return filename.path(*this, m_profile, pth, counter, complex, maxLength, shouldFixFilename, getFull); -} - /** * Try to guess the size of the image in pixels for sorting. * @return The guessed number of pixels in the image. */ int Image::value() const { + QSize size = m_sizes[Image::Size::Full]->size; + // Get from image size - if (!m_size.isEmpty()) - return m_size.width() * m_size.height(); + if (!size.isEmpty()) { + return size.width() * size.height(); + } // Get from tags - if (hasTag("incredibly_absurdres")) + if (hasTag("incredibly_absurdres")) { return 10000 * 10000; - if (hasTag("absurdres")) + } + if (hasTag("absurdres")) { return 3200 * 2400; - if (hasTag("highres")) + } + if (hasTag("highres")) { return 1600 * 1200; - if (hasTag("lowres")) + } + if (hasTag("lowres")) { return 500 * 500; + } return 1200 * 900; } -Image::SaveResult Image::save(const QString &path, bool force, bool basic, bool addMd5, bool startCommands, int count, bool postSave) +Image::SaveResult Image::save(const QString &path, Size size, bool force, bool basic, bool addMd5, bool startCommands, int count, bool postSave) { SaveResult res = SaveResult::Saved; QFile f(path); - if (!f.exists() || force) - { + if (!f.exists() || force) { const QPair md5action = m_profile->md5Action(md5()); const QString &whatToDo = md5action.first; const QString &md5Duplicate = md5action.second; // Only create the destination directory if we're going to put a file there - if (md5Duplicate.isEmpty() || force || whatToDo != "ignore") - { + if (md5Duplicate.isEmpty() || force || whatToDo != "ignore") { const QString p = path.section(QDir::separator(), 0, -2); QDir pathToFile(p), dir; - if (!pathToFile.exists() && !dir.mkpath(p)) - { + if (!pathToFile.exists() && !dir.mkpath(p)) { log(QStringLiteral("Impossible to create the destination folder: %1.").arg(p), Logger::Error); return SaveResult::Error; } } - if (md5Duplicate.isEmpty() || whatToDo == "save" || force) - { - if (!m_savePath.isEmpty() && QFile::exists(m_savePath)) - { - log(QStringLiteral("Saving image in `%1` (from `%2`)").arg(path, m_savePath)); - QFile(m_savePath).copy(path); - } - else - { - if (m_data.isEmpty()) - { return SaveResult::NotLoaded; } - - log(QStringLiteral("Saving image in `%1`").arg(path)); - - if (f.open(QFile::WriteOnly)) - { - if (f.write(m_data) < 0) - { - f.close(); - f.remove(); - log(QStringLiteral("File saving error: %1)").arg(f.errorString()), Logger::Error); - return SaveResult::Error; - } - f.close(); - } - else - { - log(QStringLiteral("Unable to open file")); - return SaveResult::Error; - } + if (md5Duplicate.isEmpty() || whatToDo == "save" || force) { + const QString savePath = m_sizes[size]->save(path); + if (!savePath.isEmpty()) { + log(QStringLiteral("Saving image in `%1` (from `%2`)").arg(path, savePath)); + } else { + return SaveResult::NotLoaded; } - } - else if (whatToDo == "copy") - { + } else if (whatToDo == "copy") { log(QStringLiteral("Copy from `%1` to `%2`").arg(md5Duplicate, path)); QFile(md5Duplicate).copy(path); res = SaveResult::Copied; - } - else if (whatToDo == "move") - { + } else if (whatToDo == "move") { log(QStringLiteral("Moving from `%1` to `%2`").arg(md5Duplicate, path)); QFile::rename(md5Duplicate, path); m_profile->setMd5(md5(), path); res = SaveResult::Moved; - } - else if (whatToDo == "link") - { + } else if (whatToDo == "link") { log(QStringLiteral("Creating link for `%1` in `%2`").arg(md5Duplicate, path)); #ifdef Q_OS_WIN @@ -549,48 +599,46 @@ Image::SaveResult Image::save(const QString &path, bool force, bool basic, bool #endif res = SaveResult::Linked; - } - else - { - log(QStringLiteral("MD5 \"%1\" of the image `%2` already found in file `%3`").arg(md5(), url().toString(), md5Duplicate)); + } else { + log(QStringLiteral("MD5 \"%1\" of the image `%2` already found in file `%3`").arg(md5(), m_url.toString(), md5Duplicate)); return SaveResult::AlreadyExistsMd5; } - if (postSave) - { postSaving(path, addMd5 && res == SaveResult::Saved, startCommands, count, basic); } + if (postSave) { + postSaving(path, size, addMd5 && res == SaveResult::Saved, startCommands, count, basic); + } + } else { + res = SaveResult::AlreadyExistsDisk; } - else - { res = SaveResult::AlreadyExistsDisk; } return res; } -void Image::postSaving(const QString &path, bool addMd5, bool startCommands, int count, bool basic) +void Image::postSaving(const QString &path, Size size, bool addMd5, bool startCommands, int count, bool basic) { - if (addMd5) - { m_profile->addMd5(md5(), path); } + if (addMd5) { + m_profile->addMd5(md5(), path); + } // Save info to a text file - if (!basic) - { + if (!basic) { auto logFiles = getExternalLogFiles(m_settings); - for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) - { + for (auto it = logFiles.constBegin(); it != logFiles.constEnd(); ++it) { auto logFile = it.value(); - const QString textfileFormat = logFile["content"].toString(); - QStringList cont = this->path(textfileFormat, "", count, true, false, false, false); - if (!cont.isEmpty()) - { + const Filename textfileFormat = Filename(logFile["content"].toString()); + QStringList cont = textfileFormat.path(*this, m_profile, "", count, Filename::Complex); + if (!cont.isEmpty()) { const int locationType = logFile["locationType"].toInt(); QString contents = cont.first(); // File path QString fileTagsPath; - if (locationType == 0) - fileTagsPath = this->path(logFile["filename"].toString(), logFile["path"].toString(), 0, true, true, true, true).first(); - else if (locationType == 1) + if (locationType == 0) { + fileTagsPath = this->paths(logFile["filename"].toString(), logFile["path"].toString(), 0).first(); + } else if (locationType == 1) { fileTagsPath = logFile["uniquePath"].toString(); - else if (locationType == 2) + } else if (locationType == 2) { fileTagsPath = path + logFile["suffix"].toString(); + } // Replace some post-save tokens contents.replace("%path:nobackslash%", QDir::toNativeSeparators(path).replace("\\", "/")) @@ -599,10 +647,10 @@ void Image::postSaving(const QString &path, bool addMd5, bool startCommands, int // Append to file if necessary QFile fileTags(fileTagsPath); const bool append = fileTags.exists(); - if (fileTags.open(QFile::WriteOnly | QFile::Append | QFile::Text)) - { - if (append) + if (fileTags.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { + if (append) { fileTags.write("\n"); + } fileTags.write(contents.toUtf8()); fileTags.close(); } @@ -611,8 +659,9 @@ void Image::postSaving(const QString &path, bool addMd5, bool startCommands, int } // Keep original date - if (m_settings->value("Save/keepDate", true).toBool()) + if (m_settings->value("Save/keepDate", true).toBool()) { setFileCreationDate(path, createdAt()); + } // Commands Commands &commands = m_profile->getCommands(); @@ -626,19 +675,20 @@ void Image::postSaving(const QString &path, bool addMd5, bool startCommands, int if (startCommands) { commands.after(); } - setSavePath(path); + setSavePath(path, size); } -QMap Image::save(const QStringList &paths, bool addMd5, bool startCommands, int count, bool force) +QMap Image::save(const QStringList &paths, bool addMd5, bool startCommands, int count, bool force, Size size) { QMap res; - for (const QString &path : paths) - res.insert(path, save(path, force, false, addMd5, startCommands, count)); + for (const QString &path : paths) { + res.insert(path, save(path, size, force, false, addMd5, startCommands, count)); + } return res; } -QMap Image::save(const QString &filename, const QString &path, bool addMd5, bool startCommands, int count) +QMap Image::save(const QString &filename, const QString &path, bool addMd5, bool startCommands, int count, Size size) { - const QStringList paths = this->path(filename, path, count, true, true, true, true); - return save(paths, addMd5, startCommands, count, false); + const QStringList paths = this->paths(filename, path, count); + return save(paths, addMd5, startCommands, count, false, size); } QList Image::filteredTags(const QStringList &remove) const @@ -648,185 +698,161 @@ QList Image::filteredTags(const QStringList &remove) const QRegExp reg; reg.setCaseSensitivity(Qt::CaseInsensitive); reg.setPatternSyntax(QRegExp::Wildcard); - for (const Tag &tag : m_tags) - { + for (const Tag &tag : m_tags) { bool removed = false; - for (const QString &rem : remove) - { + for (const QString &rem : remove) { reg.setPattern(rem); - if (reg.exactMatch(tag.text())) - { + if (reg.exactMatch(tag.text())) { removed = true; break; } } - if (!removed) + if (!removed) { tags.append(tag); + } } return tags; } -const QUrl &Image::url() const { return m_url; } -const QString &Image::rating() const { return m_rating; } Site *Image::parentSite() const { return m_parentSite; } const QList &Image::tags() const { return m_tags; } const QList &Image::pools() const { return m_pools; } qulonglong Image::id() const { return m_id; } -int Image::fileSize() const { return m_fileSize; } -int Image::width() const { return m_size.width(); } -int Image::height() const { return m_size.height(); } +int Image::fileSize() const { return m_sizes[Image::Size::Full]->fileSize; } +int Image::width() const { return size(Image::Size::Full).width(); } +int Image::height() const { return size(Image::Size::Full).height(); } +const QString &Image::rating() const { return m_rating; } +const QStringList &Image::search() const { return m_search; } const QDateTime &Image::createdAt() const { return m_createdAt; } const QUrl &Image::fileUrl() const { return m_fileUrl; } const QUrl &Image::pageUrl() const { return m_pageUrl; } -QSize Image::size() const { return m_size; } +QSize Image::size(Size size) const { return m_sizes[size]->size; } const QString &Image::name() const { return m_name; } -QPixmap Image::previewImage() const { return m_imagePreview; } -const QPixmap &Image::previewImage() { return m_imagePreview; } +QPixmap Image::previewImage() const { return m_sizes[Image::Size::Thumbnail]->pixmap(); } +const QPixmap &Image::previewImage() { return m_sizes[Image::Size::Thumbnail]->pixmap(); } Page *Image::page() const { return m_parent; } -const QByteArray &Image::data() const { return m_data; } -const QStringList &Image::search() const { return m_search; } bool Image::isGallery() const { return m_isGallery; } ExtensionRotator *Image::extensionRotator() const { return m_extensionRotator; } QString Image::extension() const { return getExtension(m_url).toLower(); } +void Image::setPromoteDetailParsWarn(bool val) { m_detailsParsWarnAsErr = val; } void Image::setPreviewImage(const QPixmap &preview) { - m_imagePreview = !m_previewRect.isNull() - ? preview.copy(m_previewRect) - : preview; + m_sizes[Image::Size::Thumbnail]->setPixmap(preview); } -void Image::setTemporaryPath(const QString &path) +void Image::setTemporaryPath(const QString &path, Size size) { - setSavePath(path); - - if (m_temporaryPath == path) - return; - - if (!m_temporaryPath.isEmpty()) - QFile::remove(m_temporaryPath); - - m_temporaryPath = path; - - if (m_fileSize <= 0) - { - m_fileSize = QFileInfo(m_temporaryPath).size(); + if (m_sizes[size]->setTemporaryPath(path)) { refreshTokens(); } } -void Image::setSavePath(const QString &path) +void Image::setSavePath(const QString &path, Size size) { - if (path != m_savePath) - { - m_savePath = path; - - if (m_fileSize <= 0) - { m_fileSize = QFileInfo(m_savePath).size(); } - + if (m_sizes[size]->setSavePath(path)) { refreshTokens(); } } -QString Image::savePath() const -{ return m_savePath; } +QString Image::savePath(Size size) const +{ return m_sizes[size]->savePath(); } -bool Image::shouldDisplaySample() const +Image::Size Image::preferredDisplaySize() const { const bool getOriginals = m_settings->value("Save/downloadoriginals", true).toBool(); const bool viewSample = m_settings->value("Zoom/viewSamples", false).toBool(); - return !m_sampleUrl.isEmpty() && (!getOriginals || viewSample); + return !m_sampleUrl.isEmpty() && (!getOriginals || viewSample) + ? Size::Sample + : Size::Full; } -QUrl Image::getDisplayableUrl() const -{ return shouldDisplaySample() ? m_sampleUrl : m_url; } QStringList Image::tagsString() const { QStringList tags; tags.reserve(m_tags.count()); - for (const Tag &tag : m_tags) + for (const Tag &tag : m_tags) { tags.append(tag.text()); + } return tags; } void Image::setUrl(const QUrl &url) { - setFileSize(0); + setFileSize(0, Size::Full); // FIXME emit urlChanged(m_url, url); m_url = url; refreshTokens(); } -void Image::setSize(QSize size) { m_size = size; refreshTokens(); } -void Image::setFileSize(int size) { m_fileSize = size; refreshTokens(); } -void Image::setData(const QByteArray &data) +void Image::setSize(QSize size, Size s) { - m_data = data; - - // Detect file extension from data headers - const bool headerDetection = m_settings->value("Save/headerDetection", true).toBool(); - if (headerDetection) - { - QString ext = getExtensionFromHeader(m_data.left(12)); - const QString currentExt = getExtension(m_url); - if (!ext.isEmpty() && ext != currentExt) - { - log(QStringLiteral("Setting image extension from header: '%1' (was '%2').").arg(ext, currentExt), Logger::Info); - setFileExtension(ext); - } - } - - // Set MD5 by hashing this data if we don't already have it - if (m_md5.isEmpty()) - { - m_md5 = QCryptographicHash::hash(m_data, QCryptographicHash::Md5).toHex(); - refreshTokens(); - } + m_sizes[s]->size = size; + refreshTokens(); +} +void Image::setFileSize(int fileSize, Size s) +{ + m_sizes[s]->fileSize = fileSize; + refreshTokens(); } void Image::setTags(const QList &tags) { m_tags = tags; refreshTokens(); } +void Image::setParentGallery(const QSharedPointer &parentGallery) +{ + m_parentGallery = parentGallery; + refreshTokens(); +} QColor Image::color() const { // Blacklisted QStringList detected = m_profile->getBlacklist().match(tokens(m_profile)); - if (!detected.isEmpty()) + if (!detected.isEmpty()) { return { 0, 0, 0 }; + } // Favorited (except for exact favorite search) auto favorites = m_profile->getFavorites(); - for (const Tag &tag : m_tags) - if (!m_parent->search().contains(tag.text())) - for (const Favorite &fav : favorites) - if (fav.getName() == tag.text()) - { return { 255, 192, 203 }; } + for (const Tag &tag : m_tags) { + if (!m_parent->search().contains(tag.text())) { + for (const Favorite &fav : favorites) { + if (fav.getName() == tag.text()) { + return { 255, 192, 203 }; + } + } + } + } // Image with a parent - if (m_parentId != 0) + if (m_parentId != 0) { return { 204, 204, 0 }; + } // Image with children - if (m_hasChildren) + if (m_hasChildren) { return { 0, 255, 0 }; + } // Pending image - if (m_status == "pending") + if (m_status == "pending") { return { 0, 0, 255 }; + } return {}; } QString Image::tooltip() const { - if (m_isGallery) + if (m_isGallery) { return QStringLiteral("%1%2") .arg(m_id == 0 ? " " : tr("ID: %1
").arg(m_id)) .arg(m_name.isEmpty() ? " " : tr("Name: %1
").arg(m_name)); + } - double size = m_fileSize; + double size = m_sizes[Image::Size::Full]->fileSize; const QString unit = getUnit(&size); return QStringLiteral("%1%2%3%4%5%6%7%8") @@ -835,8 +861,8 @@ QString Image::tooltip() const .arg(m_rating.isEmpty() ? " " : tr("Rating: %1
").arg(m_rating)) .arg(m_hasScore ? tr("Score: %1
").arg(m_score) : " ") .arg(m_author.isEmpty() ? " " : tr("User: %1

").arg(m_author)) - .arg(m_size.width() == 0 || m_size.height() == 0 ? " " : tr("Size: %1 x %2
").arg(QString::number(m_size.width()), QString::number(m_size.height()))) - .arg(m_fileSize == 0 ? " " : tr("Filesize: %1 %2
").arg(QString::number(size), unit)) + .arg(width() == 0 || height() == 0 ? " " : tr("Size: %1 x %2
").arg(QString::number(width()), QString::number(height()))) + .arg(m_sizes[Image::Size::Full]->fileSize == 0 ? " " : tr("Filesize: %1 %2
").arg(QString::number(size), unit)) .arg(!m_createdAt.isValid() ? " " : tr("Date: %1").arg(m_createdAt.toString(tr("'the 'MM/dd/yyyy' at 'hh:mm")))); } @@ -852,8 +878,9 @@ QList Image::detailsData() const const QString no = tr("no"); QString sources; - for (const QString &source : m_sources) - { sources += (!sources.isEmpty() ? "
" : "") + QString("%1").arg(source); } + for (const QString &source : m_sources) { + sources += (!sources.isEmpty() ? "
" : "") + QString("%1").arg(source); + } return { @@ -866,8 +893,8 @@ QList Image::detailsData() const QStrP(tr("Author"), !m_author.isEmpty() ? m_author : unknown), QStrP(), QStrP(tr("Date"), m_createdAt.isValid() ? m_createdAt.toString(tr("'the' MM/dd/yyyy 'at' hh:mm")) : unknown), - QStrP(tr("Size"), !m_size.isEmpty() ? QString::number(m_size.width()) + "x" + QString::number(m_size.height()) : unknown), - QStrP(tr("Filesize"), m_fileSize != 0 ? formatFilesize(m_fileSize) : unknown), + QStrP(tr("Size"), !size().isEmpty() ? QString::number(width()) + "x" + QString::number(height()) : unknown), + QStrP(tr("Filesize"), m_sizes[Image::Size::Full]->fileSize != 0 ? formatFilesize(m_sizes[Image::Size::Full]->fileSize) : unknown), QStrP(), QStrP(tr("Page"), !m_pageUrl.isEmpty() ? QString("%1").arg(m_pageUrl.toString()) : unknown), QStrP(tr("URL"), !m_fileUrl.isEmpty() ? QString("%1").arg(m_fileUrl.toString()) : unknown), @@ -884,25 +911,17 @@ QList Image::detailsData() const QString Image::md5() const { + const QString savePath = m_sizes[Image::Size::Full]->savePath(); + // If we know the path to the image or its content but not its md5, we calculate it first - if (m_md5.isEmpty() && (!m_savePath.isEmpty() || !m_data.isEmpty())) - { + if (m_md5.isEmpty() && !savePath.isEmpty()) { QCryptographicHash hash(QCryptographicHash::Md5); - // Calculate from image data - if (!m_data.isEmpty()) - { - hash.addData(m_data); - } - // Calculate from image path - else - { - QFile f(m_savePath); - f.open(QFile::ReadOnly); - hash.addData(&f); - f.close(); - } + QFile f(savePath); + f.open(QFile::ReadOnly); + hash.addData(&f); + f.close(); m_md5 = hash.result().toHex(); } @@ -913,32 +932,23 @@ QString Image::md5() const bool Image::hasTag(QString tag) const { tag = tag.trimmed(); - for (const Tag &t : m_tags) - if (QString::compare(t.text(), tag, Qt::CaseInsensitive) == 0) - return true; - return false; -} -bool Image::hasAnyTag(const QStringList &tags) const -{ - for (const QString &tag : tags) - if (this->hasTag(tag)) + for (const Tag &t : m_tags) { + if (QString::compare(t.text(), tag, Qt::CaseInsensitive) == 0) { return true; + } + } return false; } -bool Image::hasAllTags(const QStringList &tags) const -{ - for (const QString &tag : tags) - if (!this->hasTag(tag)) - return false; - return true; -} bool Image::hasUnknownTag() const { - if (m_tags.isEmpty()) + if (m_tags.isEmpty()) { return true; - for (const Tag &tag : qAsConst(m_tags)) - if (tag.type().isUnknown()) + } + for (const Tag &tag : qAsConst(m_tags)) { + if (tag.type().isUnknown()) { return true; + } + } return false; } @@ -974,11 +984,13 @@ QString Image::isAnimated() const { QString ext = getExtension(m_url).toLower(); - if (ext == "gif" || ext == "apng") + if (ext == "gif" || ext == "apng") { return ext; + } - if (ext == "png" && (hasTag(QStringLiteral("animated")) || hasTag(QStringLiteral("animated_png")))) + if (ext == "png" && (hasTag(QStringLiteral("animated")) || hasTag(QStringLiteral("animated_png")))) { return QStringLiteral("apng"); + } return QString(); } @@ -996,8 +1008,9 @@ QUrl Image::url(Size size) const void Image::preload(const Filename &filename) { - if (filename.needExactTags(m_parentSite) == 0) + if (filename.needExactTags(m_parentSite) == 0) { return; + } QEventLoop loop; QObject::connect(this, &Image::finishedLoadingTags, &loop, &QEventLoop::quit); @@ -1005,9 +1018,13 @@ void Image::preload(const Filename &filename) loop.exec(); } +QStringList Image::paths(const QString &filename, const QString &folder, int count) const +{ + return paths(Filename(filename), folder, count); +} QStringList Image::paths(const Filename &filename, const QString &folder, int count) const { - return path(filename.getFormat(), folder, count, true, true, true, true); + return filename.path(*this, m_profile, folder, count, Filename::Complex | Filename::Path); } QMap Image::generateTokens(Profile *profile) const @@ -1025,43 +1042,47 @@ QMap Image::generateTokens(Profile *profile) const // Metadata tokens.insert("filename", Token(QUrl::fromPercentEncoding(m_url.fileName().section('.', 0, -2).toUtf8()), "")); - tokens.insert("website", Token(m_parentSite->url(), "")); - tokens.insert("websitename", Token(m_parentSite->name(), "")); - tokens.insert("md5", Token(md5(), "")); - tokens.insert("date", Token(m_createdAt, QDateTime())); - tokens.insert("id", Token(m_id, 0)); + tokens.insert("website", Token(m_parentSite->url())); + tokens.insert("websitename", Token(m_parentSite->name())); + tokens.insert("md5", Token(md5())); + tokens.insert("date", Token(m_createdAt)); + tokens.insert("id", Token(m_id)); tokens.insert("rating", Token(m_rating, "unknown")); - tokens.insert("score", Token(m_score, 0)); - tokens.insert("height", Token(m_size.height(), 0)); - tokens.insert("width", Token(m_size.width(), 0)); - tokens.insert("mpixels", Token(m_size.width() * m_size.height(), 0)); - tokens.insert("url_file", Token(m_url, "")); - tokens.insert("url_sample", Token(m_sampleUrl.toString(), "")); - tokens.insert("url_thumbnail", Token(m_previewUrl.toString(), "")); - tokens.insert("url_page", Token(m_pageUrl.toString(), "")); - tokens.insert("source", Token(!m_sources.isEmpty() ? m_sources.first() : "", "")); + tokens.insert("score", Token(m_score)); + tokens.insert("height", Token(height())); + tokens.insert("width", Token(width())); + tokens.insert("mpixels", Token(width() * height())); + tokens.insert("url_file", Token(m_url)); + tokens.insert("url_original", Token(m_fileUrl.toString())); + tokens.insert("url_sample", Token(m_sampleUrl.toString())); + tokens.insert("url_thumbnail", Token(m_previewUrl.toString())); + tokens.insert("url_page", Token(m_pageUrl.toString())); + tokens.insert("source", Token(!m_sources.isEmpty() ? m_sources.first() : "")); tokens.insert("sources", Token(m_sources)); - tokens.insert("filesize", Token(m_fileSize, 0)); - tokens.insert("author", Token(m_author, "")); - tokens.insert("authorid", Token(m_authorId, 0)); - tokens.insert("parentid", Token(m_parentId, 0)); + tokens.insert("filesize", Token(m_sizes[Image::Size::Full]->fileSize)); + tokens.insert("author", Token(m_author)); + tokens.insert("authorid", Token(m_authorId)); + tokens.insert("parentid", Token(m_parentId)); + tokens.insert("name", Token(m_name)); + tokens.insert("position", Token(m_position > 0 ? QString::number(m_position) : "")); // Flags - tokens.insert("has_children", Token(m_hasChildren, false)); - tokens.insert("has_note", Token(m_hasNote, false)); - tokens.insert("has_comments", Token(m_hasComments, false)); + tokens.insert("has_children", Token(m_hasChildren)); + tokens.insert("has_note", Token(m_hasNote)); + tokens.insert("has_comments", Token(m_hasComments)); // Search - for (int i = 0; i < m_search.size(); ++i) - { tokens.insert("search_" + QString::number(i + 1), Token(m_search[i], "")); } - for (int i = m_search.size(); i < 10; ++i) - { tokens.insert("search_" + QString::number(i + 1), Token("", "")); } - tokens.insert("search", Token(m_search.join(' '), "")); + for (int i = 0; i < m_search.size(); ++i) { + tokens.insert("search_" + QString::number(i + 1), Token(m_search[i])); + } + for (int i = m_search.size(); i < 10; ++i) { + tokens.insert("search_" + QString::number(i + 1), Token("")); + } + tokens.insert("search", Token(m_search.join(' '))); // Tags QMap details; - for (const Tag &tag : filteredTags(remove)) - { + for (const Tag &tag : filteredTags(remove)) { const QString &t = tag.text(); details[ignore.contains(t, Qt::CaseInsensitive) ? "general" : tag.type().name()].append(t); @@ -1074,57 +1095,67 @@ QMap Image::generateTokens(Profile *profile) const } // Shorten copyrights - if (settings->value("Save/copyright_useshorter", true).toBool()) - { + if (settings->value("Save/copyright_useshorter", true).toBool()) { QStringList copyrights; - for (const QString &cop : details["copyright"]) - { + for (const QString &cop : details["copyright"]) { bool found = false; - for (QString ©right : copyrights) - { - if (copyright.left(cop.size()) == cop.left(copyright.size())) - { - if (cop.size() < copyright.size()) - { copyright = cop; } + for (QString ©right : copyrights) { + if (copyright.left(cop.size()) == cop.left(copyright.size())) { + if (cop.size() < copyright.size()) { + copyright = cop; + } found = true; } } - if (!found) - { copyrights.append(cop); } + if (!found) { + copyrights.append(cop); + } } details["copyright"] = copyrights; } // Tags tokens.insert("general", Token(details["general"])); - tokens.insert("artist", Token(details["artist"], "replaceAll", "anonymous", "multiple artists")); - tokens.insert("copyright", Token(details["copyright"], "replaceAll", "misc", "crossover")); - tokens.insert("character", Token(details["character"], "replaceAll", "unknown", "group")); - tokens.insert("model", Token(details["model"], "replaceAll", "unknown", "multiple")); - tokens.insert("species", Token(details["species"], "replaceAll", "unknown", "multiple")); - tokens.insert("meta", Token(details["meta"], "replaceAll", "none", "multiple")); + tokens.insert("artist", Token(details["artist"], "keepAll", "anonymous", "multiple artists")); + tokens.insert("copyright", Token(details["copyright"], "keepAll", "misc", "crossover")); + tokens.insert("character", Token(details["character"], "keepAll", "unknown", "group")); + tokens.insert("model", Token(details["model"] + details["idol"], "keepAll", "unknown", "multiple")); + tokens.insert("photo_set", Token(details["photo_set"], "keepAll", "unknown", "multiple")); + tokens.insert("species", Token(details["species"], "keepAll", "unknown", "multiple")); + tokens.insert("meta", Token(details["meta"], "keepAll", "none", "multiple")); tokens.insert("allos", Token(details["allos"])); - tokens.insert("allo", Token(details["allos"].join(' '), "")); + tokens.insert("allo", Token(details["allos"].join(' '))); tokens.insert("tags", Token(details["alls"])); tokens.insert("all", Token(details["alls"])); tokens.insert("all_namespaces", Token(details["alls_namespaces"])); // Extension QString ext = extension(); - if (settings->value("Save/noJpeg", true).toBool() && ext == "jpeg") + if (settings->value("Save/noJpeg", true).toBool() && ext == "jpeg") { ext = "jpg"; + } tokens.insert("ext", Token(ext, "jpg")); tokens.insert("filetype", Token(ext, "jpg")); + // Variables + if (!m_parentGallery.isNull()) { + tokens.insert("gallery", Token([this, profile]() { return QVariant::fromValue(m_parentGallery->tokens(profile)); })); + } + + // Extra tokens + for (auto it = m_data.constBegin(); it != m_data.constEnd(); ++it) { + tokens.insert(it.key(), Token(it.value())); + } + return tokens; } -Image::SaveResult Image::preSave(const QString &path) +Image::SaveResult Image::preSave(const QString &path, Size size) { - return save(path, false, false, false, false, 1, false); + return save(path, size, false, false, false, false, 1, false); } -void Image::postSave(const QString &path, SaveResult res, bool addMd5, bool startCommands, int count) +void Image::postSave(const QString &path, Size size, SaveResult res, bool addMd5, bool startCommands, int count) { - postSaving(path, addMd5 && res == SaveResult::Saved, startCommands, count); + postSaving(path, size, addMd5 && res == SaveResult::Saved, startCommands, count); } diff --git a/lib/src/models/image.h b/lib/src/models/image.h index 341d0014c..6c79e3413 100644 --- a/lib/src/models/image.h +++ b/lib/src/models/image.h @@ -8,13 +8,15 @@ #include #include #include +#include "image-size.h" #include "loader/downloadable.h" +#include "loader/token.h" +#include "models/pool.h" #include "tags/tag.h" class ExtensionRotator; class Page; -class Pool; class Profile; class QSettings; class Site; @@ -26,58 +28,57 @@ class Image : public QObject, public Downloadable public: Image(); Image(Site *site, QMap details, Profile *profile, Page *parent = nullptr); + Image(Site *site, QMap details, QVariantMap data, Profile *profile, Page *parent = nullptr); Image(const Image &other); - ~Image(); + + // Serialization + void write(QJsonObject &json) const; + bool read(const QJsonObject &json, const QMap &sites); + + // TODO(Bionus): remove these two methods + QMap save(const QString &filename, const QString &path, bool addMd5 = true, bool startCommands = false, int count = 1, Size size = Size::Full); + int value() const; - QStringList path(QString fn, QString pth, int counter = 0, bool complex = true, bool maxLength = true, bool shouldFixFilename = true, bool getFull = false) const; - QStringList stylishedTags(Profile *profile) const; - SaveResult save(const QString &path, bool force = false, bool basic = false, bool addMd5 = true, bool startCommands = false, int count = 1, bool postSave = true); - void postSaving(const QString &path, bool addMd5 = true, bool startCommands = false, int count = 1, bool basic = false); - QMap save(const QStringList &paths, bool addMd5 = true, bool startCommands = false, int count = 1, bool force = false); - QMap save(const QString &filename, const QString &path, bool addMd5 = true, bool startCommands = false, int count = 1); QString md5() const; - const QUrl &url() const; - const QString &rating() const; const QList &tags() const; - QList filteredTags(const QStringList &remove) const; QStringList tagsString() const; const QList &pools() const; qulonglong id() const; int fileSize() const; int width() const; int height() const; + const QString &rating() const; + const QStringList &search() const; const QDateTime &createdAt() const; const QUrl &pageUrl() const; const QUrl &fileUrl() const; - QSize size() const; + QSize size(Size size = Size::Full) const; const QString &name() const; - QPixmap previewImage() const; - const QPixmap &previewImage(); - void setPreviewImage(const QPixmap &preview); Page *page() const; - const QByteArray &data() const; - const QStringList &search() const; Site *parentSite() const; ExtensionRotator *extensionRotator() const; bool hasTag(QString tag) const; - bool hasAnyTag(const QStringList &tags) const; - bool hasAllTags(const QStringList &tags) const; bool hasUnknownTag() const; void setUrl(const QUrl &url); - void setData(const QByteArray &data); - void setSize(QSize size); - void setFileSize(int size); + void setSize(QSize size, Size s); + void setFileSize(int fileSize, Size s); void setFileExtension(const QString &ext); - void setTemporaryPath(const QString &path); - void setSavePath(const QString &path); - QString savePath() const; - bool shouldDisplaySample() const; - QUrl getDisplayableUrl() const; + void setTemporaryPath(const QString &path, Size size = Size::Full); + void setSavePath(const QString &path, Size size = Size::Full); + QString savePath(Size size = Size::Full) const; + Size preferredDisplaySize() const; bool isVideo() const; QString isAnimated() const; void setTags(const QList &tags); bool isGallery() const; QString extension() const; + void setParentGallery(const QSharedPointer &parentGallery); + void setPromoteDetailParsWarn(bool); + + // Preview pixmap store + QPixmap previewImage() const; + const QPixmap &previewImage(); + void setPreviewImage(const QPixmap &preview); // Displayable QColor color() const override; @@ -86,12 +87,13 @@ class Image : public QObject, public Downloadable QList detailsData() const override; // Downloadable - QUrl url(Size size) const override; + QUrl url(Size size = Size::Full) const override; void preload(const Filename &filename) override; + QStringList paths(const QString &filename, const QString &folder, int count) const; QStringList paths(const Filename &filename, const QString &folder, int count) const override; QMap generateTokens(Profile *profile) const override; - SaveResult preSave(const QString &path) override; - void postSave(const QString &path, SaveResult result, bool addMd5, bool startCommands, int count) override; + SaveResult preSave(const QString &path, Size size) override; + void postSave(const QString &path, Size size, SaveResult result, bool addMd5, bool startCommands, int count) override; // Templates /*template @@ -101,8 +103,14 @@ class Image : public QObject, public Downloadable }*/ protected: + QList filteredTags(const QStringList &remove) const; void setRating(const QString &rating); + // Saving + SaveResult save(const QString &path, Size size, bool force = false, bool basic = false, bool addMd5 = true, bool startCommands = false, int count = 1, bool postSave = true); + void postSaving(const QString &path, Size size, bool addMd5 = true, bool startCommands = false, int count = 1, bool basic = false); + QMap save(const QStringList &paths, bool addMd5 = true, bool startCommands = false, int count = 1, bool force = false, Size size = Size::Full); + public slots: void loadDetails(bool rateLimit = false); void loadDetailsNow(); @@ -118,22 +126,17 @@ class Image : public QObject, public Downloadable Profile *m_profile; Page *m_parent; qulonglong m_id; - int m_score, m_parentId, m_fileSize, m_authorId; + int m_score, m_parentId, m_authorId; bool m_hasChildren, m_hasNote, m_hasComments, m_hasScore; QUrl m_url; QString mutable m_md5; - QString m_author, m_name, m_status, m_rating, m_site, m_temporaryPath, m_savePath; + QString m_author, m_name, m_status, m_rating; QStringList m_sources; QUrl m_pageUrl, m_fileUrl, m_sampleUrl, m_previewUrl; - QSize m_size; - QPixmap m_imagePreview; - QRect m_previewRect; QDateTime m_createdAt; - QByteArray m_data; QNetworkReply *m_loadDetails; QList m_tags; QList m_pools; - QTime m_timer; QSettings *m_settings; QStringList m_search; Site *m_parentSite; @@ -141,6 +144,11 @@ class Image : public QObject, public Downloadable bool m_loadingDetails, m_loadedDetails; bool m_isGallery = false; int m_galleryCount; + int m_position; + bool m_detailsParsWarnAsErr = false; + QSharedPointer m_parentGallery; + QMap> m_sizes; + QVariantMap m_data; }; Q_DECLARE_METATYPE(Image) diff --git a/lib/src/models/md5-database.cpp b/lib/src/models/md5-database.cpp new file mode 100644 index 000000000..132674757 --- /dev/null +++ b/lib/src/models/md5-database.cpp @@ -0,0 +1,150 @@ +#include "models/md5-database.h" +#include +#include + + +Md5Database::Md5Database(QString path, QSettings *settings) + : m_path(std::move(path)), m_settings(settings), m_flushTimer(this) +{ + // Read all MD5 from the database and load them in memory + QFile fileMD5(m_path); + if (fileMD5.open(QFile::ReadOnly | QFile::Text)) { + QString line; + while (!(line = fileMD5.readLine()).isEmpty()) { + m_md5s.insert(line.left(32), line.mid(32).trimmed()); + } + + fileMD5.close(); + } + + // Connect the timer to the flush slot + m_flushTimer.setSingleShot(true); + m_flushTimer.setInterval(m_settings->value("md5_flush_interval", 1000).toInt()); + connect(&m_flushTimer, &QTimer::timeout, this, &Md5Database::flush); +} + +Md5Database::~Md5Database() +{ + sync(); +} + + +/** + * Appends the newly-added MD5s to the MD5 file. + */ +void Md5Database::flush() +{ + if (m_path.isEmpty()) { + return; + } + + QFile fileMD5(m_path); + if (fileMD5.open(QFile::Text | QFile::WriteOnly | QFile::Append)) { + for (auto it = m_pendingAdd.begin(); it != m_pendingAdd.end(); ++it) { + fileMD5.write(QString(it.key() + it.value() + "\n").toUtf8()); + } + + fileMD5.close(); + } + + m_pendingAdd.clear(); + emit flushed(); +} + +/** + * Rewrites the whole contents of the MD5 file with the current database. + */ +void Md5Database::sync() +{ + if (m_path.isEmpty()) { + return; + } + + QFile fileMD5(m_path); + if (fileMD5.open(QFile::Text | QFile::WriteOnly | QFile::Truncate)) { + for (auto it = m_md5s.begin(); it != m_md5s.end(); ++it) { + fileMD5.write(QString(it.key() + it.value() + "\n").toUtf8()); + } + + fileMD5.close(); + } +} + +QPair Md5Database::action(const QString &md5) +{ + QString action = m_settings->value("Save/md5Duplicates", "save").toString(); + const bool keepDeleted = m_settings->value("Save/keepDeletedMd5", false).toBool(); + + const bool contains = !md5.isEmpty() && m_md5s.contains(md5); + QString path = contains ? m_md5s[md5] : QString(); + const bool exists = contains && QFile::exists(path); + + if (contains && !exists) { + if (!keepDeleted) { + remove(md5); + path = QString(); + } else { + action = "ignore"; + } + } + + return QPair(action, path); +} + +/** + * Check if a file with this md5 already exists; + * @param md5 The md5 that needs to be checked. + * @return A QString containing the path to the already existing file, an empty QString if the md5 does not already exists. + */ +QString Md5Database::exists(const QString &md5) +{ + if (m_md5s.contains(md5)) { + if (QFile::exists(m_md5s[md5])) { + return m_md5s[md5]; + } + + if (!m_settings->value("Save/keepDeletedMd5", false).toBool()) { + remove(md5); + } + } + return QString(); +} + +/** + * Adds a md5 to the _md5 map and adds it to the md5 file. + * @param md5 The md5 to add. + * @param path The path to the image with this md5. + */ +void Md5Database::add(const QString &md5, const QString &path) +{ + if (!md5.isEmpty()) { + m_md5s.insert(md5, path); + + m_pendingAdd.insert(md5, path); + if (m_pendingAdd.count() >= 100) { + m_flushTimer.stop(); + flush(); + } else { + m_flushTimer.start(); + } + } +} + +/** + * Set a md5 to the _md5 map changing the file it is pointing to. + * @param md5 The md5 to add. + * @param path The path to the image with this md5. + */ +void Md5Database::set(const QString &md5, const QString &path) +{ + m_md5s[md5] = path; +} + +/** + * Removes a md5 from the _md5 map and removes it from the md5 file. + * @param md5 The md5 to remove. + */ +void Md5Database::remove(const QString &md5) +{ + m_md5s.remove(md5); +} diff --git a/lib/src/models/md5-database.h b/lib/src/models/md5-database.h new file mode 100644 index 000000000..b474ab96e --- /dev/null +++ b/lib/src/models/md5-database.h @@ -0,0 +1,42 @@ +#ifndef MD5_DATABASE_H +#define MD5_DATABASE_H + +#include +#include +#include +#include +#include + + +class QSettings; + +class Md5Database : public QObject +{ + Q_OBJECT + + public: + explicit Md5Database(QString file, QSettings *settings); + ~Md5Database() override; + void sync(); + + QPair action(const QString &md5); + QString exists(const QString &md5); + void add(const QString &md5, const QString &path); + void set(const QString &md5, const QString &path); + void remove(const QString &md5); + + protected slots: + void flush(); + + signals: + void flushed(); + + private: + QString m_path; + QSettings *m_settings; + QHash m_md5s; + QTimer m_flushTimer; + QHash m_pendingAdd; +}; + +#endif // MD5_DATABASE_H diff --git a/lib/src/models/page-api.cpp b/lib/src/models/page-api.cpp index eba9b51b1..3bddc2965 100755 --- a/lib/src/models/page-api.cpp +++ b/lib/src/models/page-api.cpp @@ -9,12 +9,13 @@ #include "models/api/api.h" #include "models/filtering/post-filter.h" #include "models/page.h" +#include "models/search-query/search-query.h" #include "models/site.h" #include "tags/tag.h" -PageApi::PageApi(Page *parentPage, Profile *profile, Site *site, Api *api, QStringList tags, int page, int limit, PostFilter postFiltering, bool smart, QObject *parent, int pool, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId) - : QObject(parent), m_parentPage(parentPage), m_profile(profile), m_site(site), m_api(api), m_search(std::move(tags)), m_errors(QStringList()), m_postFiltering(std::move(postFiltering)), m_imagesPerPage(limit), m_lastPage(lastPage), m_lastPageMinId(lastPageMinId), m_lastPageMaxId(lastPageMaxId), m_smart(smart), m_reply(nullptr), m_replyTags(nullptr) +PageApi::PageApi(Page *parentPage, Profile *profile, Site *site, Api *api, SearchQuery query, int page, int limit, PostFilter postFiltering, bool smart, QObject *parent, int pool, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId) + : QObject(parent), m_parentPage(parentPage), m_profile(profile), m_site(site), m_api(api), m_query(std::move(query)), m_errors(QStringList()), m_postFiltering(std::move(postFiltering)), m_imagesPerPage(limit), m_lastPage(lastPage), m_lastPageMinId(lastPageMinId), m_lastPageMaxId(lastPageMaxId), m_smart(smart), m_reply(nullptr), m_replyTags(nullptr) { m_imagesCount = -1; m_maxImagesCount = -1; @@ -39,10 +40,9 @@ void PageApi::setLastPage(Page *page) m_lastPageMaxId = page->maxId(); m_lastPageMinId = page->minId(); - if (!page->nextPage().isEmpty()) - { m_url = page->nextPage(); } - else - { /*fallback(false);*/ } + if (!page->nextPage().isEmpty()) { + m_url = page->nextPage(); + } updateUrls(); } @@ -50,27 +50,24 @@ void PageApi::setLastPage(Page *page) void PageApi::updateUrls() { QString url; - QString search = m_search.join(' '); m_errors.clear(); - // Gallery searches using either 'gallery:url' or 'gallery:id' - const bool isGallery = m_search.count() == 1 && search.startsWith("gallery:"); - if (isGallery) - { search = search.mid(8); } - // URL searches - if (m_search.count() == 1 && !search.isEmpty() && isUrl(search)) - { url = search; } - else - { + if (!m_query.url.isEmpty()) { + url = m_query.url; + } else if (m_query.tags.count() == 1 && isUrl(m_query.tags.first())) { + url = m_query.tags.first(); + } else { PageUrl ret; - if (isGallery) - { ret = m_api->galleryUrl(search, m_page, m_imagesPerPage, m_site); } - else - { ret = m_api->pageUrl(search, m_page, m_imagesPerPage, m_lastPage, m_lastPageMinId, m_lastPageMaxId, m_site); } + if (!m_query.gallery.isNull()) { + ret = m_api->galleryUrl(m_query.gallery, m_page, m_imagesPerPage, m_site); + } else { + ret = m_api->pageUrl(m_query.tags.join(' '), m_page, m_imagesPerPage, m_lastPage, m_lastPageMinId, m_lastPageMaxId, m_site); + } - if (!ret.error.isEmpty()) - { m_errors.append(ret.error); } + if (!ret.error.isEmpty()) { + m_errors.append(ret.error); + } url = ret.url; } @@ -79,39 +76,39 @@ void PageApi::updateUrls() m_originalUrl = QString(url); m_url = QString(url); - m_urlRegex = QUrl(url); } void PageApi::setReply(QNetworkReply *reply) { - if (m_reply != nullptr) - { - if (m_reply->isRunning()) + if (m_reply != nullptr) { + if (m_reply->isRunning()) { m_reply->abort(); + } m_reply->deleteLater(); } - if (m_replyTimer->isActive()) + if (m_replyTimer->isActive()) { m_replyTimer->stop(); + } m_reply = reply; } void PageApi::load(bool rateLimit, bool force) { - if (m_loading) - { - if (!force) + if (m_loading) { + if (!force) { return; + } setReply(nullptr); } - if (m_url.isEmpty() && !m_errors.isEmpty()) - { - for (const QString &err : qAsConst(m_errors)) - { log(QStringLiteral("[%1][%2] %3").arg(m_site->url(), m_format, err), Logger::Warning); } + if (m_url.isEmpty() && !m_errors.isEmpty()) { + for (const QString &err : qAsConst(m_errors)) { + log(QStringLiteral("[%1][%2] %3").arg(m_site->url(), m_format, err), Logger::Warning); + } emit finishedLoading(this, LoadResult::Error); return; } @@ -128,16 +125,16 @@ void PageApi::load(bool rateLimit, bool force) // Load the request with a possible delay int ms = m_site->msToRequest(rateLimit ? Site::QueryType::Retry : Site::QueryType::List); - if (ms > 0) - { - if (m_replyTimer->isActive()) + if (ms > 0) { + if (m_replyTimer->isActive()) { m_replyTimer->stop(); + } m_replyTimer->setInterval(ms); m_replyTimer->start(); + } else { + loadNow(); } - else - { loadNow(); } } void PageApi::loadNow() { @@ -147,22 +144,24 @@ void PageApi::loadNow() } void PageApi::abort() { - if (m_replyTimer->isActive()) + if (m_replyTimer->isActive()) { m_replyTimer->stop(); - if (m_reply != nullptr && m_reply->isRunning()) + } + if (m_reply != nullptr && m_reply->isRunning()) { m_reply->abort(); + } } bool PageApi::addImage(const QSharedPointer &img) { - if (img.isNull()) + if (img.isNull()) { return false; + } m_pageImageCount++; QStringList filters = m_postFiltering.match(img->tokens(m_profile)); - if (!filters.isEmpty()) - { + if (!filters.isEmpty()) { img->deleteLater(); log(QStringLiteral("[%1][%2] Image filtered. Reason: %3.").arg(m_site->url(), m_format, filters.join(", ")), Logger::Info); return false; @@ -174,25 +173,25 @@ bool PageApi::addImage(const QSharedPointer &img) void PageApi::parse() { - if (m_reply == nullptr) + if (m_reply == nullptr) { return; + } log(QStringLiteral("[%1][%2] Receiving page `%3`").arg(m_site->url(), m_format, m_reply->url().toString().toHtmlEscaped()), Logger::Info); // Check redirection QUrl redir = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redir.isEmpty()) - { + if (!redir.isEmpty()) { QUrl newUrl = m_site->fixUrl(redir.toString(), m_url); log(QStringLiteral("[%1][%2] Redirecting page `%3` to `%4`").arg(m_site->url(), m_format, m_url.toString().toHtmlEscaped(), newUrl.toString().toHtmlEscaped()), Logger::Info); // HTTP -> HTTPS redirects const bool ssl = m_site->setting("ssl", false).toBool(); - if (!ssl && newUrl.path() == m_url.path() && newUrl.scheme() == "https" && m_url.scheme() == "http") - { + if (!ssl && newUrl.path() == m_url.path() && newUrl.scheme() == "https" && m_url.scheme() == "http") { const bool notThisSite = m_site->setting("ssl_never_correct", false).toBool(); - if (!notThisSite) - { emit httpsRedirect(); } + if (!notThisSite) { + emit httpsRedirect(); + } } m_url = newUrl; @@ -202,8 +201,7 @@ void PageApi::parse() // Detect HTTP 429 usage limit reached const int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode == 429) - { + if (statusCode == 429) { log(QStringLiteral("[%1][%2] Limit reached (429). New try.").arg(m_site->url(), m_format), Logger::Warning); load(true, true); return; @@ -215,86 +213,99 @@ void PageApi::parse() void PageApi::parseActual() { + const bool isGallery = !m_query.gallery.isNull(); + const bool parseErrors = isGallery ? m_api->parseGalleryErrors() : m_api->parsePageErrors(); + const int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const int offset = (m_page - 1) * m_imagesPerPage; + // Try to read the reply m_source = m_reply->readAll(); - if (m_source.isEmpty() || m_reply->error() != QNetworkReply::NoError) - { - if (m_reply->error() != QNetworkReply::OperationCanceledError) - { log(QStringLiteral("[%1][%2] Loading error: %3 (%4)").arg(m_site->url(), m_format, m_reply->errorString()).arg(m_reply->error()), Logger::Error); } + if (m_source.isEmpty() || (m_reply->error() != QNetworkReply::NoError && !parseErrors)) { + if (m_reply->error() != QNetworkReply::OperationCanceledError) { + log(QStringLiteral("[%1][%2] Loading error: %3 (%4)").arg(m_site->url(), m_format, m_reply->errorString()).arg(m_reply->error()), Logger::Error); + } setReply(nullptr); + m_loaded = true; + m_loading = false; emit finishedLoading(this, LoadResult::Error); return; } - const int first = m_smart && m_blim > 0 ? ((m_page - 1) * m_imagesPerPage) % m_blim : 0; - // Parse source ParsedPage page; - if (m_search.count() == 1 && m_search[0].startsWith("gallery:")) - { page = m_api->parseGallery(m_parentPage, m_source, first); } - else - { page = m_api->parsePage(m_parentPage, m_source, first); } + if (isGallery) { + page = m_api->parseGallery(m_parentPage, m_source, statusCode, offset); + } else { + page = m_api->parsePage(m_parentPage, m_source, statusCode, offset); + } // Handle errors - if (!page.error.isEmpty()) - { + if (!page.error.isEmpty()) { m_errors.append(page.error); log(QStringLiteral("[%1][%2] %3").arg(m_site->url(), m_format, page.error), Logger::Warning); setReply(nullptr); + m_loaded = true; + m_loading = false; emit finishedLoading(this, LoadResult::Error); return; } // Fill data from parsing result - if (page.pageCount >= 0) - { setPageCount(page.pageCount, true); } - if (page.imageCount >= 0) - { setImageCount(page.imageCount, true); } - for (const Tag &tag : qAsConst(page.tags)) - { m_tags.append(tag); } - for (const QSharedPointer &img : qAsConst(page.images)) - { addImage(img); } - if (page.urlNextPage.isValid()) - { m_urlNextPage = page.urlNextPage; } - if (page.urlPrevPage.isValid()) - { m_urlPrevPage = page.urlPrevPage; } - if (!page.wiki.isEmpty()) - { m_wiki = fixCloudflareEmails(page.wiki); } + if (page.pageCount >= 0) { + setPageCount(page.pageCount, true); + } + if (page.imageCount >= 0) { + setImageCount(page.imageCount, true); + } + for (const Tag &tag : qAsConst(page.tags)) { + m_tags.append(tag); + } + for (const QSharedPointer &img : qAsConst(page.images)) { + addImage(img); + } + if (page.urlNextPage.isValid()) { + m_urlNextPage = page.urlNextPage; + } + if (page.urlPrevPage.isValid()) { + m_urlPrevPage = page.urlPrevPage; + } + if (!page.wiki.isEmpty()) { + m_wiki = fixCloudflareEmails(page.wiki); + } + + // Link images to their respective galleries + if (isGallery) { + for (auto &img : m_images) { + img->setParentGallery(m_query.gallery); + } + } // Complete image count information from tag count information - if (m_imagesCount < 1 || !m_imagesCountSafe) - { + if (m_imagesCount < 1 || !m_imagesCountSafe) { int found = 0; int min = -1; - for (const Tag &tag : qAsConst(m_tags)) - { - if (m_search.contains(tag.text())) - { + for (const Tag &tag : qAsConst(m_tags)) { + if (m_query.tags.contains(tag.text())) { found++; - if (min == -1 || min > tag.count()) - { min = tag.count(); } + if (min == -1 || min > tag.count()) { + min = tag.count(); + } } } - int searchTagsCount = m_search.count();; - if (m_search.count() > found) - { + int searchTagsCount = m_query.tags.count();; + if (m_query.tags.count() > found) { const QStringList modifiers = QStringList() << "-" << m_api->modifiers(); - for (const QString &search : qAsConst(m_search)) - { - for (const QString &modifier : modifiers) - { - if (search.startsWith(modifier)) - { + for (const QString &search : qAsConst(m_query.tags)) { + for (const QString &modifier : modifiers) { + if (search.startsWith(modifier)) { searchTagsCount--; break; } } } } - if (searchTagsCount == found) - { - if (m_search.count() == 1) - { + if (searchTagsCount == found) { + if (m_query.tags.count() == 1) { const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; const int expectedPageCount = qCeil(static_cast(min) / perPage); @@ -305,20 +316,14 @@ void PageApi::parseActual() } // Complete missing tag information from images' tags if necessary - if (m_tags.isEmpty()) - { + if (m_tags.isEmpty()) { QStringList tagsGot; - for (const QSharedPointer &img : qAsConst(m_images)) - { - for (const Tag &tag : img->tags()) - { - if (tagsGot.contains(tag.text())) - { + for (const QSharedPointer &img : qAsConst(m_images)) { + for (const Tag &tag : img->tags()) { + if (tagsGot.contains(tag.text())) { const int index = tagsGot.indexOf(tag.text()); m_tags[index].setCount(m_tags[index].count() + 1); - } - else - { + } else { m_tags.append(tag); tagsGot.append(tag.text()); } @@ -328,39 +333,37 @@ void PageApi::parseActual() // Remove first n images (according to site settings) int skip = m_site->setting("ignore/always", 0).toInt(); - if (false && m_isAltPage) // FIXME(Bionus): broken since move to Api class - { skip = m_site->setting("ignore/alt", 0).toInt(); } - if (m_page == 1) - { skip = m_site->setting("ignore/1", 0).toInt(); } - if (m_api->getName() == QLatin1String("Html")) - { - if (m_images.size() >= skip) - { - for (int i = 0; i < skip; ++i) - { + if (false && m_isAltPage) { // FIXME(Bionus): broken since move to Api class + skip = m_site->setting("ignore/alt", 0).toInt(); + } + if (m_page == 1) { + skip = m_site->setting("ignore/1", 0).toInt(); + } + if (m_api->getName() == QLatin1String("Html")) { + if (m_images.size() >= skip) { + for (int i = 0; i < skip; ++i) { m_images.removeFirst(); m_pageImageCount--; } + } else { + log(QStringLiteral("Wanting to skip %1 images but only %2 returned").arg(skip).arg(m_images.size()), Logger::Warning); } - else - { log(QStringLiteral("Wanting to skip %1 images but only %2 returned").arg(skip).arg(m_images.size()), Logger::Warning); } } // Virtual paging int firstImage = 0; int lastImage = m_smart ? m_imagesPerPage : m_images.size(); - if (false && !m_originalUrl.contains("{page}") && !m_originalUrl.contains("{cpage}") && !m_originalUrl.contains("{pagepart}") && !m_originalUrl.contains("{pid}")) // TODO(Bionus): add real virtual paging - { + if (false && !m_originalUrl.contains("{page}") && !m_originalUrl.contains("{cpage}") && !m_originalUrl.contains("{pagepart}") && !m_originalUrl.contains("{pid}")) { // TODO(Bionus): add real virtual paging firstImage = m_imagesPerPage * (m_page - 1); lastImage = m_imagesPerPage; } - while (firstImage > 0 && !m_images.isEmpty()) - { + while (firstImage > 0 && !m_images.isEmpty()) { m_images.removeFirst(); firstImage--; } - while (m_images.size() > lastImage) - { m_images.removeLast(); } + while (m_images.size() > lastImage) { + m_images.removeLast(); + } log(QStringLiteral("[%1][%2] Parsed page `%3`: %4 images (%5), %6 tags (%7), %8 total (%9), %10 pages (%11)").arg(m_site->url(), m_format, m_reply->url().toString().toHtmlEscaped()).arg(m_images.count()).arg(m_pageImageCount).arg(page.tags.count()).arg(m_tags.count()).arg(imagesCount(false)).arg(imagesCount(true)).arg(pagesCount(false)).arg(pagesCount(true)), Logger::Info); @@ -382,7 +385,6 @@ const QUrl &PageApi::url() const { return m_url; } const QString &PageApi::source() const { return m_source; } const QString &PageApi::wiki() const { return m_wiki; } const QList &PageApi::tags() const { return m_tags; } -const QStringList &PageApi::search() const { return m_search; } const QStringList &PageApi::errors() const { return m_errors; } const QUrl &PageApi::nextPage() const { return m_urlNextPage; } const QUrl &PageApi::prevPage() const { return m_urlPrevPage; } @@ -401,8 +403,9 @@ bool PageApi::hasNext() const { int pageCount = pagesCount(); int maxPages = maxPagesCount(); - if (pageCount <= 0 && maxPages > 0) + if (pageCount <= 0 && maxPages > 0) { pageCount = maxPages; + } return pageCount > m_page || (pageCount <= 0 && m_pageImageCount > 0); } @@ -410,17 +413,19 @@ bool PageApi::hasNext() const bool PageApi::isImageCountSure() const { return m_imagesCountSafe; } int PageApi::imagesCount(bool guess) const { - if (m_imagesCountSafe) + if (m_imagesCountSafe) { return m_imagesCount; + } - if (m_pagesCount == 1) + if (m_pagesCount == 1) { return m_pageImageCount; + } - if (!guess) + if (!guess) { return -1; + } - if (m_imagesCount < 0 && m_pagesCount >= 0) - { + if (m_imagesCount < 0 && m_pagesCount >= 0) { const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; return m_pagesCount * perPage; @@ -433,14 +438,15 @@ int PageApi::maxImagesCount() const bool PageApi::isPageCountSure() const { return m_pagesCountSafe; } int PageApi::pagesCount(bool guess) const { - if (m_pagesCountSafe) + if (m_pagesCountSafe) { return m_pagesCount; + } - if (!guess) + if (!guess) { return -1; + } - if (m_pagesCount < 0 && m_imagesCount >= 0) - { + if (m_pagesCount < 0 && m_imagesCount >= 0) { const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; return qCeil(static_cast(m_imagesCount) / perPage); @@ -450,8 +456,9 @@ int PageApi::pagesCount(bool guess) const } int PageApi::maxPagesCount() const { - if (m_maxImagesCount < 0) + if (m_maxImagesCount < 0) { return -1; + } const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; @@ -461,29 +468,31 @@ int PageApi::maxPagesCount() const qulonglong PageApi::maxId() const { qulonglong maxId = 0; - for (const QSharedPointer &img : m_images) - if (img->id() > maxId || maxId == 0) + for (const QSharedPointer &img : m_images) { + if (img->id() > maxId || maxId == 0) { maxId = img->id(); + } + } return maxId; } qulonglong PageApi::minId() const { qulonglong minId = 0; - for (const QSharedPointer &img : m_images) - if (img->id() < minId || minId == 0) + for (const QSharedPointer &img : m_images) { + if (img->id() < minId || minId == 0) { minId = img->id(); + } + } return minId; } void PageApi::setImageCount(int count, bool sure) { - if (m_imagesCount <= 0 || (!m_imagesCountSafe && sure)) - { + if (m_imagesCount <= 0 || (!m_imagesCountSafe && sure)) { m_imagesCount = count; m_imagesCountSafe = sure; - if (sure) - { + if (sure) { const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; setPageCount(qCeil(static_cast(count) / perPage), true); @@ -495,13 +504,11 @@ void PageApi::setImageMaxCount(int maxCount) void PageApi::setPageCount(int count, bool sure) { - if (m_pagesCount <= 0 || (!m_pagesCountSafe && sure)) - { + if (m_pagesCount <= 0 || (!m_pagesCountSafe && sure)) { m_pagesCount = count; m_pagesCountSafe = sure; - if (sure) - { + if (sure) { const int forcedLimit = m_api->forcedLimit(); const int perPage = forcedLimit > 0 ? forcedLimit : m_imagesPerPage; setImageCount(count * perPage, false); diff --git a/lib/src/models/page-api.h b/lib/src/models/page-api.h index 9702fe930..6a614a284 100644 --- a/lib/src/models/page-api.h +++ b/lib/src/models/page-api.h @@ -6,6 +6,7 @@ #include #include #include "models/filtering/post-filter.h" +#include "models/search-query/search-query.h" #include "tags/tag.h" @@ -15,6 +16,7 @@ class Page; class Profile; class QNetworkReply; class QTimer; +class SearchQuery; class Site; class PageApi : public QObject @@ -29,7 +31,7 @@ class PageApi : public QObject Error }; - explicit PageApi(Page *parentPage, Profile *profile, Site *site, Api *api, QStringList tags = QStringList(), int page = 1, int limit = 25, PostFilter postFiltering = PostFilter(), bool smart = false, QObject *parent = nullptr, int pool = 0, int lastPage = 0, qulonglong lastPageMinId = 0, qulonglong lastPageMaxId = 0); + explicit PageApi(Page *parentPage, Profile *profile, Site *site, Api *api, SearchQuery query, int page = 1, int limit = 25, PostFilter postFiltering = PostFilter(), bool smart = false, QObject *parent = nullptr, int pool = 0, int lastPage = 0, qulonglong lastPageMinId = 0, qulonglong lastPageMaxId = 0); void setLastPage(Page *page); const QList> &images() const; bool isImageCountSure() const; @@ -42,7 +44,6 @@ class PageApi : public QObject const QString &source() const; const QString &wiki() const; const QList &tags() const; - const QStringList &search() const; const QStringList &errors() const; int imagesPerPage() const; int highLimit() const; @@ -81,13 +82,14 @@ class PageApi : public QObject Profile *m_profile; Site *m_site; Api *m_api; - QStringList m_search, m_errors; + SearchQuery m_query; + QStringList m_errors; PostFilter m_postFiltering; - int m_imagesPerPage, m_lastPage, m_page, m_blim, m_pool; + int m_imagesPerPage, m_lastPage, m_page, m_pool; qulonglong m_lastPageMinId, m_lastPageMaxId; bool m_smart, m_isAltPage; QString m_format, m_source, m_wiki, m_originalUrl; - QUrl m_url, m_urlRegex, m_urlNextPage, m_urlPrevPage; + QUrl m_url, m_urlNextPage, m_urlPrevPage; QList> m_images; QList m_tags; QNetworkReply *m_reply, *m_replyTags; diff --git a/lib/src/models/page.cpp b/lib/src/models/page.cpp index c60b25ac0..ae7e615f1 100644 --- a/lib/src/models/page.cpp +++ b/lib/src/models/page.cpp @@ -3,51 +3,60 @@ #include "functions.h" #include "logger.h" #include "models/api/api.h" +#include "models/search-query/search-query.h" #include "models/site.h" -Page::Page(Profile *profile, Site *site, const QList &sites, QStringList tags, int page, int limit, const QStringList &postFiltering, bool smart, QObject *parent, int pool, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId) - : QObject(parent), m_site(site), m_regexApi(-1), m_errors(QStringList()), m_imagesPerPage(limit), m_lastPage(lastPage), m_lastPageMinId(lastPageMinId), m_lastPageMaxId(lastPageMaxId), m_smart(smart) +Page::Page(Profile *profile, Site *site, const QList &sites, SearchQuery query, int page, int limit, const QStringList &postFiltering, bool smart, QObject *parent, int pool, int lastPage, qulonglong lastPageMinId, qulonglong lastPageMaxId) + : QObject(parent), m_site(site), m_regexApi(-1), m_query(std::move(query)), m_errors(QStringList()), m_imagesPerPage(limit), m_smart(smart) { m_website = m_site->url(); m_imagesCount = -1; m_pagesCount = -1; - // Replace shortcuts to increase compatibility - QString text = " " + tags.join(' ') + " "; - text.replace(" rating:s ", " rating:safe ", Qt::CaseInsensitive) - .replace(" rating:q ", " rating:questionable ", Qt::CaseInsensitive) - .replace(" rating:e ", " rating:explicit ", Qt::CaseInsensitive) - .replace(" -rating:s ", " -rating:safe ", Qt::CaseInsensitive) - .replace(" -rating:q ", " -rating:questionable ", Qt::CaseInsensitive) - .replace(" -rating:e ", " -rating:explicit ", Qt::CaseInsensitive); - tags = text.split(" ", QString::SkipEmptyParts); - - // Get the list of all enabled modifiers - QStringList modifiers = QStringList(); - for (Site *ste : sites) - { modifiers.append(ste->getApis().first()->modifiers()); } - const QStringList mods = m_site->getApis().first()->modifiers(); - for (const QString &mod : mods) - { modifiers.removeAll(mod); } - - // Remove modifiers from tags - for (const QString &mod : modifiers) - { tags.removeAll(mod); } - m_search = tags; + if (!m_query.tags.isEmpty()) { + // Replace shortcuts to increase compatibility + QString text = " " + m_query.tags.join(' ') + " "; + text.replace(" rating:s ", " rating:safe ", Qt::CaseInsensitive) + .replace(" rating:q ", " rating:questionable ", Qt::CaseInsensitive) + .replace(" rating:e ", " rating:explicit ", Qt::CaseInsensitive) + .replace(" -rating:s ", " -rating:safe ", Qt::CaseInsensitive) + .replace(" -rating:q ", " -rating:questionable ", Qt::CaseInsensitive) + .replace(" -rating:e ", " -rating:explicit ", Qt::CaseInsensitive); + QStringList tags = text.split(" ", QString::SkipEmptyParts); + + // Get the list of all enabled modifiers + QStringList modifiers = QStringList(); + for (Site *ste : sites) { + modifiers.append(ste->getApis().first()->modifiers()); + } + const QStringList mods = m_site->getApis().first()->modifiers(); + for (const QString &mod : mods) { + modifiers.removeAll(mod); + } + + // Remove modifiers from tags + for (const QString &mod : modifiers) { + tags.removeAll(mod); + } + m_search = tags; + + m_query.tags = m_search; + } // Generate pages PostFilter postFilter(postFiltering); m_siteApis = m_site->getLoggedInApis(); m_pageApis.reserve(m_siteApis.count()); - for (Api *api : qAsConst(m_siteApis)) - { - auto *pageApi = new PageApi(this, profile, m_site, api, m_search, page, limit, postFilter, smart, parent, pool, lastPage, lastPageMinId, lastPageMaxId); - if (m_pageApis.count() == 0) - { connect(pageApi, &PageApi::httpsRedirect, this, &Page::httpsRedirectSlot); } + for (Api *api : qAsConst(m_siteApis)) { + auto *pageApi = new PageApi(this, profile, m_site, api, m_query, page, limit, postFilter, smart, parent, pool, lastPage, lastPageMinId, lastPageMaxId); + if (m_pageApis.count() == 0) { + connect(pageApi, &PageApi::httpsRedirect, this, &Page::httpsRedirectSlot); + } m_pageApis.append(pageApi); - if (api->getName() == QLatin1String("Html") && m_regexApi < 0) - { m_regexApi = m_pageApis.count() - 1; } + if (api->getName() == QLatin1String("Html") && m_regexApi < 0) { + m_regexApi = m_pageApis.count() - 1; + } } m_currentApi = -1; @@ -65,8 +74,7 @@ void Page::fallback(bool loadIfPossible) { m_errors.clear(); - if (m_currentApi >= m_siteApis.count() - 1) - { + if (m_currentApi >= m_siteApis.count() - 1) { log(QStringLiteral("[%1] No valid source of the site returned result.").arg(m_site->url()), Logger::Warning); m_errors.append(tr("No valid source of the site returned result.")); emit failedLoading(this); @@ -74,29 +82,23 @@ void Page::fallback(bool loadIfPossible) } m_currentApi++; - if (m_currentApi > 0) + if (m_currentApi > 0) { log(QStringLiteral("[%1] Loading using %2 failed. Retry using %3.").arg(m_site->url(), m_siteApis[m_currentApi - 1]->getName(), m_siteApis[m_currentApi]->getName()), Logger::Warning); + } - if (loadIfPossible) + if (loadIfPossible) { load(); + } } void Page::setLastPage(Page *page) { - m_lastPage = page->page(); - m_lastPageMaxId = page->maxId(); - m_lastPageMinId = page->minId(); - - for (PageApi *api : qAsConst(m_pageApis)) + for (PageApi *api : qAsConst(m_pageApis)) { api->setLastPage(page); - - if (false && !page->nextPage().isEmpty()) - { /*m_url = page->nextPage();*/ } - else - { - m_currentApi--; - fallback(false); } + + m_currentApi--; + fallback(false); } void Page::load(bool rateLimit) @@ -106,15 +108,16 @@ void Page::load(bool rateLimit) } void Page::loadFinished(PageApi *api, PageApi::LoadResult status) { - if (api != m_pageApis[m_currentApi]) + if (api != m_pageApis[m_currentApi]) { return; + } - if (status == PageApi::LoadResult::Ok) + if (status == PageApi::LoadResult::Ok) { emit finishedLoading(this); - else - { - if (!api->errors().isEmpty()) + } else { + if (!api->errors().isEmpty()) { m_errors.append(api->errors()); + } fallback(); } } @@ -125,8 +128,9 @@ void Page::abort() void Page::loadTags() { - if (m_regexApi < 0) + if (m_regexApi < 0) { return; + } connect(m_pageApis[m_regexApi], &PageApi::finishedLoading, this, &Page::loadTagsFinished); m_pageApis[m_regexApi]->load(); @@ -135,15 +139,17 @@ void Page::loadTagsFinished(PageApi *api, PageApi::LoadResult status) { Q_UNUSED(status); - if (m_regexApi < 0 || api != m_pageApis[m_regexApi]) + if (m_regexApi < 0 || api != m_pageApis[m_regexApi]) { return; + } emit finishedLoadingTags(this); } void Page::abortTags() { - if (m_regexApi < 0) + if (m_regexApi < 0) { return; + } m_pageApis[m_regexApi]->abort(); } @@ -154,13 +160,15 @@ void Page::httpsRedirectSlot() void Page::clear() { - for (PageApi *pageApi : qAsConst(m_pageApis)) + for (PageApi *pageApi : qAsConst(m_pageApis)) { pageApi->clear(); + } } Site *Page::site() const { return m_site; } const QString &Page::website() const { return m_website; } const QString &Page::wiki() const { return m_pageApis[m_regexApi < 0 ? m_currentApi : m_regexApi]->wiki(); } +const SearchQuery &Page::query() const { return m_query; } const QStringList &Page::search() const { return m_search; } const QStringList &Page::errors() const { return m_errors; } int Page::imagesPerPage() const { return m_imagesPerPage; } @@ -174,52 +182,55 @@ const QUrl &Page::nextPage() const { return m_pageApis[m_currentApi]->nextPage() const QUrl &Page::prevPage() const { return m_pageApis[m_currentApi]->prevPage(); } int Page::highLimit() const { return m_pageApis[m_currentApi]->highLimit(); } bool Page::hasNext() const { return m_pageApis[m_currentApi]->hasNext(); } +bool Page::isLoaded() const { return m_pageApis[m_currentApi]->isLoaded(); } bool Page::hasSource() const { - for (auto pageApi : qAsConst(m_pageApis)) - if (!pageApi->source().isEmpty()) + for (auto pageApi : qAsConst(m_pageApis)) { + if (!pageApi->source().isEmpty()) { return true; + } + } return false; } int Page::imagesCount(bool guess) const { - if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isImageCountSure()) - { + if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isImageCountSure()) { const int count = m_pageApis[m_regexApi]->imagesCount(guess); - if (count >= 0) + if (count >= 0) { return count; + } } return m_pageApis[m_currentApi]->imagesCount(guess); } int Page::maxImagesCount() const { - if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isImageCountSure()) - { + if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isImageCountSure()) { const int count = m_pageApis[m_regexApi]->maxImagesCount(); - if (count >= 0) + if (count >= 0) { return count; + } } return m_pageApis[m_currentApi]->maxImagesCount(); } int Page::pagesCount(bool guess) const { - if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isPageCountSure()) - { + if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isPageCountSure()) { const int count = m_pageApis[m_regexApi]->pagesCount(guess); - if (count >= 0) + if (count >= 0) { return count; + } } return m_pageApis[m_currentApi]->pagesCount(guess); } int Page::maxPagesCount() const { - if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isPageCountSure()) - { + if (m_regexApi >= 0 && !m_pageApis[m_currentApi]->isPageCountSure()) { const int count = m_pageApis[m_regexApi]->maxPagesCount(); - if (count >= 0) + if (count >= 0) { return count; + } } return m_pageApis[m_currentApi]->maxPagesCount(); } diff --git a/lib/src/models/page.h b/lib/src/models/page.h index d87ccee52..f160d9774 100644 --- a/lib/src/models/page.h +++ b/lib/src/models/page.h @@ -12,6 +12,7 @@ class Api; class Image; class Profile; +class SearchQuery; class Site; class Tag; @@ -20,7 +21,7 @@ class Page : public QObject Q_OBJECT public: - explicit Page(Profile *profile, Site *site, const QList &sites, QStringList tags = QStringList(), int page = 1, int limit = 25, const QStringList &postFiltering = QStringList(), bool smart = false, QObject *parent = nullptr, int pool = 0, int lastPage = 0, qulonglong lastPageMinId = 0, qulonglong lastPageMaxId = 0); + explicit Page(Profile *profile, Site *site, const QList &sites, SearchQuery query, int page = 1, int limit = 25, const QStringList &postFiltering = QStringList(), bool smart = false, QObject *parent = nullptr, int pool = 0, int lastPage = 0, qulonglong lastPageMinId = 0, qulonglong lastPageMaxId = 0); ~Page() override; void setLastPage(Page *page); void fallback(bool loadIfPossible = true); @@ -38,6 +39,7 @@ class Page : public QObject const QString &website() const; const QString &wiki() const; const QList &tags() const; + const SearchQuery &query() const; const QStringList &search() const; const QStringList &errors() const; int imagesPerPage() const; @@ -49,6 +51,7 @@ class Page : public QObject qulonglong maxId() const; const QUrl &nextPage() const; const QUrl &prevPage() const; + bool isLoaded() const; public slots: void abort(); @@ -72,11 +75,11 @@ class Page : public QObject QList m_siteApis; QList m_pageApis; int m_regexApi; + SearchQuery m_query; QStringList m_errors, m_search; - int m_imagesPerPage, m_lastPage, m_imagesCount, m_pagesCount, m_page, m_pool; - qulonglong m_lastPageMinId, m_lastPageMaxId; + int m_imagesPerPage, m_imagesCount, m_pagesCount, m_page, m_pool; bool m_smart; - QString m_format, m_website, m_source, m_originalUrl; + QString m_website; }; #endif // PAGE_H diff --git a/lib/src/models/pool.cpp b/lib/src/models/pool.cpp index 48c3f021e..a96bac3de 100644 --- a/lib/src/models/pool.cpp +++ b/lib/src/models/pool.cpp @@ -4,7 +4,7 @@ Pool::Pool(int id, QString name, int current, int next, int previous) : m_id(id), m_name(std::move(name)), m_current(current), m_next(next), m_previous(previous) -{ } +{} int Pool::id() const { return m_id; } const QString &Pool::name() const { return m_name; } diff --git a/lib/src/models/profile.cpp b/lib/src/models/profile.cpp index 8592840b4..33f1224b2 100644 --- a/lib/src/models/profile.cpp +++ b/lib/src/models/profile.cpp @@ -6,16 +6,18 @@ #include #include "commands/commands.h" #include "functions.h" +#include "logger.h" #include "models/favorite.h" +#include "models/md5-database.h" #include "models/site.h" #include "models/source.h" Profile::Profile() - : m_settings(nullptr), m_commands(nullptr) + : m_settings(nullptr), m_commands(nullptr), m_md5s(nullptr) {} Profile::Profile(QSettings *settings, QList favorites, QStringList keptForLater, QString path) - : m_path(std::move(path)), m_settings(settings), m_favorites(std::move(favorites)), m_keptForLater(std::move(keptForLater)), m_commands(nullptr) + : m_path(std::move(path)), m_settings(settings), m_favorites(std::move(favorites)), m_keptForLater(std::move(keptForLater)), m_commands(nullptr), m_md5s(nullptr) {} Profile::Profile(QString path) : m_path(std::move(path)) @@ -24,56 +26,48 @@ Profile::Profile(QString path) // Load sources QStringList dirs = QDir(m_path + "/sites/").entryList(QDir::Dirs | QDir::NoDotAndDotDot); - for (const QString &dir : dirs) - { + for (const QString &dir : dirs) { Source *source = new Source(this, m_path + "/sites/" + dir); - if (source->getApis().isEmpty()) - { + if (source->getApis().isEmpty()) { source->deleteLater(); continue; } m_sources.insert(source->getName(), source); + m_additionalTokens.append(source->getAdditionalTokens()); - for (Site *site : source->getSites()) + for (Site *site : source->getSites()) { m_sites.insert(site->url(), site); + } } // Load favorites QSet unique; QFile fileFavoritesJson(m_path + "/favorites.json"); - if (fileFavoritesJson.open(QFile::ReadOnly | QFile::Text)) - { + if (fileFavoritesJson.open(QFile::ReadOnly | QFile::Text)) { const QByteArray data = fileFavoritesJson.readAll(); QJsonDocument loadDoc = QJsonDocument::fromJson(data); QJsonObject object = loadDoc.object(); QJsonArray favorites = object["favorites"].toArray(); - for (auto favoriteJson : favorites) - { + for (auto favoriteJson : favorites) { Favorite fav = Favorite::fromJson(m_path, favoriteJson.toObject(), m_sites); - if (!unique.contains(fav.getName())) - { + if (!unique.contains(fav.getName())) { unique.insert(fav.getName()); m_favorites.append(fav); } } - } - else - { + } else { QFile fileFavorites(m_path + "/favorites.txt"); - if (fileFavorites.open(QFile::ReadOnly | QFile::Text)) - { + if (fileFavorites.open(QFile::ReadOnly | QFile::Text)) { QString favs = fileFavorites.readAll(); fileFavorites.close(); QStringList words = favs.split("\n", QString::SkipEmptyParts); m_favorites.reserve(words.count()); - for (const QString &word : words) - { + for (const QString &word : words) { Favorite fav = Favorite::fromString(m_path, word); - if (!unique.contains(fav.getName())) - { + if (!unique.contains(fav.getName())) { unique.insert(fav.getName()); m_favorites.append(fav); } @@ -83,8 +77,7 @@ Profile::Profile(QString path) // Load view it later QFile fileKfl(m_path + "/viewitlater.txt"); - if (fileKfl.open(QFile::ReadOnly | QFile::Text)) - { + if (fileKfl.open(QFile::ReadOnly | QFile::Text)) { QString vil = fileKfl.readAll(); fileKfl.close(); @@ -93,8 +86,7 @@ Profile::Profile(QString path) // Load ignored QFile fileIgnored(m_path + "/ignore.txt"); - if (fileIgnored.open(QFile::ReadOnly | QFile::Text)) - { + if (fileIgnored.open(QFile::ReadOnly | QFile::Text)) { QString ign = fileIgnored.readAll(); fileIgnored.close(); @@ -102,34 +94,26 @@ Profile::Profile(QString path) } // Load MD5s - QFile fileMD5(m_path + "/md5s.txt"); - if (fileMD5.open(QFile::ReadOnly | QFile::Text)) - { - QString line; - while (!(line = fileMD5.readLine()).isEmpty()) - m_md5s.insert(line.left(32), line.mid(32).trimmed()); - - fileMD5.close(); - } + m_md5s = new Md5Database(m_path + "/md5s.txt", m_settings); // Load auto-complete QFile fileAutoComplete(m_path + "/words.txt"); - if (fileAutoComplete.open(QFile::ReadOnly | QFile::Text)) - { + if (fileAutoComplete.open(QFile::ReadOnly | QFile::Text)) { QString line; - while (!(line = fileAutoComplete.readLine()).isEmpty()) + while (!(line = fileAutoComplete.readLine()).isEmpty()) { m_autoComplete.append(line.trimmed().split(" ", QString::SkipEmptyParts)); + } fileAutoComplete.close(); } // Load custom auto-complete QFile fileCustomAutoComplete(m_path + "/wordsc.txt"); - if (fileCustomAutoComplete.open(QFile::ReadOnly | QFile::Text)) - { + if (fileCustomAutoComplete.open(QFile::ReadOnly | QFile::Text)) { QString line; - while (!(line = fileCustomAutoComplete.readLine()).isEmpty()) + while (!(line = fileCustomAutoComplete.readLine()).isEmpty()) { m_customAutoComplete.append(line.trimmed().split(" ", QString::SkipEmptyParts)); + } fileCustomAutoComplete.close(); } @@ -138,25 +122,36 @@ Profile::Profile(QString path) // Blacklisted tags const QStringList &blacklist = m_settings->value("blacklistedtags").toString().split(' ', QString::SkipEmptyParts); - for (const QString &bl : blacklist) - { m_blacklist.add(bl); } + for (const QString &bl : blacklist) { + m_blacklist.add(bl); + } QFile fileBlacklist(m_path + "/blacklist.txt"); - if (fileBlacklist.open(QFile::ReadOnly | QFile::Text)) - { + if (fileBlacklist.open(QFile::ReadOnly | QFile::Text)) { QString line; - while (!(line = fileBlacklist.readLine()).isEmpty()) + while (!(line = fileBlacklist.readLine()).isEmpty()) { m_blacklist.add(line.trimmed().split(" ", QString::SkipEmptyParts)); + } fileBlacklist.close(); } + + // Complete auto-complete + m_autoComplete.reserve(m_autoComplete.count() + m_customAutoComplete.count() + m_favorites.count()); + m_autoComplete.append(m_customAutoComplete); + for (const Favorite &fav : qAsConst(m_favorites)) { + m_autoComplete.append(fav.getName()); + } + m_autoComplete.removeDuplicates(); + m_autoComplete.sort(); } Profile::~Profile() { sync(); - if (m_settings != nullptr) + if (m_settings != nullptr) { m_settings->deleteLater(); + } qDeleteAll(m_sources); delete m_commands; @@ -165,29 +160,20 @@ Profile::~Profile() void Profile::sync() { - if (m_path.isEmpty()) + if (m_path.isEmpty()) { return; + } syncFavorites(); syncKeptForLater(); syncIgnored(); // MD5s - QFile fileMD5(m_path + "/md5s.txt"); - if (fileMD5.open(QFile::WriteOnly | QFile::Truncate)) - { - QStringList md5s = m_md5s.keys(); - QStringList paths = m_md5s.values(); - for (int i = 0; i < md5s.size(); i++) - fileMD5.write(QString(md5s[i] + paths[i] + "\n").toUtf8()); - - fileMD5.close(); - } + m_md5s->sync(); // Custom auto-complete QFile fileCustomAutoComplete(m_path + "/wordsc.txt"); - if (fileCustomAutoComplete.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { + if (fileCustomAutoComplete.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { fileCustomAutoComplete.write(m_customAutoComplete.join("\r\n").toUtf8()); fileCustomAutoComplete.close(); } @@ -200,26 +186,24 @@ void Profile::sync() // Blacklisted tags QFile fileBlacklist(m_path + "/blacklist.txt"); - if (fileBlacklist.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { + if (fileBlacklist.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { fileBlacklist.write(m_blacklist.toString().toUtf8()); fileBlacklist.close(); } m_settings->remove("blacklistedtags"); // Sync settings - if (m_settings != nullptr) + if (m_settings != nullptr) { m_settings->sync(); + } } void Profile::syncFavorites() const { QFile fileFavorites(m_path + "/favorites.json"); - if (fileFavorites.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { + if (fileFavorites.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { // Generate JSON array QJsonArray favoritesJson; - for (const Favorite &fav : qAsConst(m_favorites)) - { + for (const Favorite &fav : qAsConst(m_favorites)) { QJsonObject unique; fav.toJson(unique); favoritesJson.append(unique); @@ -239,8 +223,7 @@ void Profile::syncFavorites() const void Profile::syncKeptForLater() const { QFile fileKfl(m_path + "/viewitlater.txt"); - if (fileKfl.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { + if (fileKfl.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { fileKfl.write(m_keptForLater.join("\r\n").toUtf8()); fileKfl.close(); } @@ -248,8 +231,7 @@ void Profile::syncKeptForLater() const void Profile::syncIgnored() const { QFile fileIgnored(m_path + "/ignore.txt"); - if (fileIgnored.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) - { + if (fileIgnored.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { fileIgnored.write(m_ignored.join("\r\n").toUtf8()); fileIgnored.close(); } @@ -271,15 +253,14 @@ void Profile::purgeTemp(int maxAge) const int purged = 0; int failed = 0; - for (const QFileInfo &tempFile : tempFiles) - { + for (const QFileInfo &tempFile : tempFiles) { const QDateTime lastModified = tempFile.lastModified(); - if (lastModified < max) - { - if (QFile::remove(tempFile.absoluteFilePath())) + if (lastModified < max) { + if (QFile::remove(tempFile.absoluteFilePath())) { purged++; - else + } else { failed++; + } } } @@ -288,9 +269,13 @@ void Profile::purgeTemp(int maxAge) const void Profile::addFavorite(const Favorite &fav) { - m_favorites.removeAll(fav); + const int already = m_favorites.removeAll(fav); m_favorites.append(fav); + if (already == 0) { + m_autoComplete.append(fav.getName()); + } + syncFavorites(); emit favoritesChanged(); } @@ -298,8 +283,9 @@ void Profile::removeFavorite(const Favorite &fav) { m_favorites.removeAll(fav); - if (QFile::exists(m_path + "/thumbs/" + fav.getName(true) + ".png")) + if (QFile::exists(m_path + "/thumbs/" + fav.getName(true) + ".png")) { QFile::remove(m_path + "/thumbs/" + fav.getName(true) + ".png"); + } syncFavorites(); emit favoritesChanged(); @@ -344,25 +330,7 @@ void Profile::removeIgnored(const QString &tag) QPair Profile::md5Action(const QString &md5) { - QString action = m_settings->value("Save/md5Duplicates", "save").toString(); - const bool keepDeleted = m_settings->value("Save/keepDeletedMd5", false).toBool(); - - const bool contains = !md5.isEmpty() && m_md5s.contains(md5); - QString path = contains ? m_md5s[md5] : QString(); - const bool exists = contains && QFile::exists(path); - - if (contains && !exists) - { - if (!keepDeleted) - { - removeMd5(md5); - path = QString(); - } - else - { action = "ignore"; } - } - - return QPair(action, path); + return m_md5s->action(md5); } /** @@ -372,15 +340,7 @@ QPair Profile::md5Action(const QString &md5) */ QString Profile::md5Exists(const QString &md5) { - if (m_md5s.contains(md5)) - { - if (QFile::exists(m_md5s[md5])) - return m_md5s[md5]; - - if (!m_settings->value("Save/keepDeletedMd5", false).toBool()) - removeMd5(md5); - } - return QString(); + return m_md5s->exists(md5); } /** @@ -390,8 +350,7 @@ QString Profile::md5Exists(const QString &md5) */ void Profile::addMd5(const QString &md5, const QString &path) { - if (!md5.isEmpty()) - { m_md5s.insert(md5, path); } + m_md5s->add(md5, path); } /** @@ -401,7 +360,7 @@ void Profile::addMd5(const QString &md5, const QString &path) */ void Profile::setMd5(const QString &md5, const QString &path) { - m_md5s[md5] = path; + m_md5s->set(md5, path); } /** @@ -410,7 +369,7 @@ void Profile::setMd5(const QString &md5, const QString &path) */ void Profile::removeMd5(const QString &md5) { - m_md5s.remove(md5); + m_md5s->remove(md5); } @@ -460,16 +419,18 @@ QStringList &Profile::getKeptForLater() { return m_keptForLater; } QStringList &Profile::getIgnored() { return m_ignored; } Commands &Profile::getCommands() { return *m_commands; } QStringList &Profile::getAutoComplete() { return m_autoComplete; } -QStringList &Profile::getCustomAutoComplete() { return m_customAutoComplete; } Blacklist &Profile::getBlacklist() { return m_blacklist; } const QMap &Profile::getSources() const { return m_sources; } const QMap &Profile::getSites() const { return m_sites; } +const QStringList &Profile::getAdditionalTokens() const { return m_additionalTokens; } QList Profile::getFilteredSites(const QStringList &urls) const { QList ret; - for (const QString &url : urls) - if (m_sites.contains(url)) + for (const QString &url : urls) { + if (m_sites.contains(url)) { ret.append(m_sites.value(url)); + } + } return ret; } diff --git a/lib/src/models/profile.h b/lib/src/models/profile.h index eae086d90..5a310abaa 100644 --- a/lib/src/models/profile.h +++ b/lib/src/models/profile.h @@ -11,6 +11,7 @@ class Commands; +class Md5Database; class QSettings; class Site; class Source; @@ -70,10 +71,10 @@ class Profile : public QObject QStringList &getIgnored(); Commands &getCommands(); QStringList &getAutoComplete(); - QStringList &getCustomAutoComplete(); Blacklist &getBlacklist(); const QMap &getSources() const; const QMap &getSites() const; + const QStringList &getAdditionalTokens() const; QList getFilteredSites(const QStringList &urls) const; private: @@ -99,9 +100,10 @@ class Profile : public QObject QStringList m_autoComplete; QStringList m_customAutoComplete; Blacklist m_blacklist; - QHash m_md5s; + Md5Database *m_md5s; QMap m_sources; QMap m_sites; + QStringList m_additionalTokens; }; #endif // PROFILE_H diff --git a/lib/src/models/search-query/gallery-search-query.cpp b/lib/src/models/search-query/gallery-search-query.cpp new file mode 100644 index 000000000..c3fa9cd7f --- /dev/null +++ b/lib/src/models/search-query/gallery-search-query.cpp @@ -0,0 +1,11 @@ +#include "models/search-query/gallery-search-query.h" + + +GallerySearchQuery::GallerySearchQuery(QSharedPointer gallery) + : m_gallery(std::move(gallery)) +{} + +QSharedPointer GallerySearchQuery::gallery() const +{ + return m_gallery; +} diff --git a/lib/src/models/search-query/gallery-search-query.h b/lib/src/models/search-query/gallery-search-query.h new file mode 100644 index 000000000..e6dfcad30 --- /dev/null +++ b/lib/src/models/search-query/gallery-search-query.h @@ -0,0 +1,20 @@ +#ifndef GALLERY_SEARCH_QUERY_H +#define GALLERY_SEARCH_QUERY_H + +#include +#include "models/search-query/search-query.h" + + +class Image; + +class GallerySearchQuery //: public SearchQuery +{ + public: + explicit GallerySearchQuery(QSharedPointer gallery); + QSharedPointer gallery() const; + + private: + QSharedPointer m_gallery; +}; + +#endif // GALLERY_SEARCH_QUERY_H diff --git a/lib/src/models/search-query/search-query.cpp b/lib/src/models/search-query/search-query.cpp new file mode 100644 index 000000000..d1f20e5b5 --- /dev/null +++ b/lib/src/models/search-query/search-query.cpp @@ -0,0 +1,64 @@ +#include "models/search-query/search-query.h" +#include +#include "functions.h" +#include "models/image.h" +#include "models/profile.h" +#include "models/site.h" + + +QString SearchQuery::toString() const +{ + if (!gallery.isNull()) { + return "Gallery: " + gallery->name(); + } + + return tags.join(' '); +} + + +void SearchQuery::write(QJsonObject &json) const +{ + json["tags"] = QJsonArray::fromStringList(tags); + + if (!gallery.isNull()) { + QJsonObject jsonGallery; + gallery->write(jsonGallery); + json["gallery"] = jsonGallery; + } +} + +bool SearchQuery::read(const QJsonObject &json, const QMap &sites) +{ + // Tags + if (json.contains("tags")) { + QJsonArray jsonTags = json["tags"].toArray(); + tags.reserve(jsonTags.count()); + for (auto tag : jsonTags) { + tags.append(tag.toString()); + } + } + + // Gallery + if (json.contains("gallery")) { + auto image = new Image(); + if (image->read(json["gallery"].toObject(), sites)) { + gallery = QSharedPointer(image); + } else { + image->deleteLater(); + } + } + + return true; +} + + +bool operator==(const SearchQuery &lhs, const SearchQuery &rhs) +{ + return lhs.tags == rhs.tags + && lhs.gallery == rhs.gallery; +} + +bool operator!=(const SearchQuery &lhs, const SearchQuery &rhs) +{ + return !(lhs == rhs); +} diff --git a/lib/src/models/search-query/search-query.h b/lib/src/models/search-query/search-query.h new file mode 100644 index 000000000..d70f3e7e4 --- /dev/null +++ b/lib/src/models/search-query/search-query.h @@ -0,0 +1,43 @@ +#ifndef SEARCH_QUERY_H +#define SEARCH_QUERY_H + +#include +#include +#include +#include +#include +#include + + +class Image; +class Site; + +class SearchQuery +{ + public: + // Constructors + SearchQuery() = default; + SearchQuery(QStringList tags_) + : tags(tags_) + {} + SearchQuery(QSharedPointer gallery_) + : gallery(gallery_) + {} + QString toString() const; + + // Serialization + void write(QJsonObject &json) const; + bool read(const QJsonObject &json, const QMap &sites); + + // Public members + QString url; + QStringList tags; + QSharedPointer gallery; +}; + +bool operator==(const SearchQuery &lhs, const SearchQuery &rhs); +bool operator!=(const SearchQuery &lhs, const SearchQuery &rhs); + +Q_DECLARE_METATYPE(SearchQuery) + +#endif // SEARCH_QUERY_H diff --git a/lib/src/models/search-query/tag-search-query.cpp b/lib/src/models/search-query/tag-search-query.cpp new file mode 100644 index 000000000..8f76dfa75 --- /dev/null +++ b/lib/src/models/search-query/tag-search-query.cpp @@ -0,0 +1,11 @@ +#include "models/search-query/tag-search-query.h" + + +TagSearchQuery::TagSearchQuery(QStringList tags) + : m_tags(std::move(tags)) +{} + +QStringList TagSearchQuery::tags() const +{ + return m_tags; +} diff --git a/lib/src/models/search-query/tag-search-query.h b/lib/src/models/search-query/tag-search-query.h new file mode 100644 index 000000000..5df19d508 --- /dev/null +++ b/lib/src/models/search-query/tag-search-query.h @@ -0,0 +1,18 @@ +#ifndef TAG_SEARCH_QUERY_H +#define TAG_SEARCH_QUERY_H + +#include +#include "models/search-query/search-query.h" + + +class TagSearchQuery //: public SearchQuery +{ + public: + explicit TagSearchQuery(QStringList tags); + QStringList tags() const; + + private: + QStringList m_tags; +}; + +#endif // TAG_SEARCH_QUERY_H diff --git a/lib/src/models/site.cpp b/lib/src/models/site.cpp index 7630ba146..1537b3478 100644 --- a/lib/src/models/site.cpp +++ b/lib/src/models/site.cpp @@ -7,6 +7,9 @@ #include #include #include +#include "auth/http-auth.h" +#include "auth/oauth2-auth.h" +#include "auth/url-auth.h" #include "custom-network-access-manager.h" #include "functions.h" #include "logger.h" @@ -56,8 +59,9 @@ void Site::loadConfig() { const QString siteDir = m_source->getPath() + "/" + m_url + "/"; - if (m_settings != nullptr) + if (m_settings != nullptr) { m_settings->deleteLater(); + } QSettings *settingsCustom = new QSettings(siteDir + "settings.ini", QSettings::IniFormat); QSettings *settingsDefaults = new QSettings(siteDir + "defaults.ini", QSettings::IniFormat); m_settings = new MixedSettings(QList() << settingsCustom << settingsDefaults); @@ -71,73 +75,89 @@ void Site::loadConfig() << pSettings->value("source_3").toString() << pSettings->value("source_4").toString(); defaults.removeAll(""); - if (defaults.isEmpty()) - { defaults = QStringList() << "Xml" << "Json" << "Regex" << "Rss"; } + if (defaults.isEmpty()) { + defaults = QStringList() << "Xml" << "Json" << "Regex" << "Rss"; + } // Get overridden source order QStringList sources; - if (!m_settings->value("sources/usedefault", true).toBool()) - { - for (int i = 0; i < 4; ++i) - { + if (!m_settings->value("sources/usedefault", true).toBool()) { + for (int i = 0; i < 4; ++i) { const QString def = defaults.count() > i ? defaults[i] : QString(); sources << m_settings->value("sources/source_" + QString::number(i + 1), def).toString(); } sources.removeAll(""); - if (sources.isEmpty()) - { sources = defaults; } + if (sources.isEmpty()) { + sources = defaults; + } + } else { + sources = defaults; + } + for (int i = 0; i < sources.count(); i++) { + sources[i][0] = sources[i][0].toUpper(); } - else - { sources = defaults; } - for (int i = 0; i < sources.count(); i++) - { sources[i][0] = sources[i][0].toUpper(); } // Apis m_apis.clear(); - for (const QString &src : qAsConst(sources)) - { + for (const QString &src : qAsConst(sources)) { Api *api = m_source->getApi(src == "Regex" ? "Html" : src); - if (api != nullptr && !m_apis.contains(api)) + if (api != nullptr && !m_apis.contains(api)) { m_apis.append(api); + } } // Auth information - const QString type = m_settings->value("login/type", "url").toString(); - if (m_login != nullptr) - m_login->deleteLater(); - if (type == "url") - m_login = new UrlLogin(this, m_manager, m_settings); - else if (type == "oauth2") - m_login = new OAuth2Login(this, m_manager, m_settings); - else if (type == "post") - m_login = new HttpPostLogin(this, m_manager, m_settings); - else if (type == "get") - m_login = new HttpGetLogin(this, m_manager, m_settings); - else - { - m_login = nullptr; - log(QStringLiteral("Invalid login type '%1'").arg(type), Logger::Error); + m_auth = nullptr; + const QString defType = m_settings->value("login/type", "url").toString(); + if (defType != "disabled") { + const auto &auths = m_source->getAuths(); + for (auto it = auths.constBegin(); it != auths.constEnd(); ++it) { + if (it.value()->type() == defType || m_auth == nullptr) { + m_auth = it.value(); + } + } + if (m_login != nullptr) { + m_login->deleteLater(); + } + if (m_auth != nullptr) { + QString type = m_auth->type(); + if (type == "url") { + m_login = new UrlLogin(dynamic_cast(m_auth), this, m_manager, m_settings); + } else if (type == "oauth2") { + m_login = new OAuth2Login(dynamic_cast(m_auth), this, m_manager, m_settings); + } else if (type == "post") { + m_login = new HttpPostLogin(dynamic_cast(m_auth), this, m_manager, m_settings); + } else if (type == "get") { + m_login = new HttpGetLogin(dynamic_cast(m_auth), this, m_manager, m_settings); + } else { + m_login = nullptr; + log(QStringLiteral("[%1] Invalid login type '%1'").arg(m_url, type), Logger::Error); + } + } else { + m_login = nullptr; + log(QStringLiteral("[%1] No auth found").arg(m_url), Logger::Error); + } } // Cookies m_cookies.clear(); QList settingsCookies = m_settings->value("cookies").toList(); - for (const QVariant &variant : settingsCookies) - { + for (const QVariant &variant : settingsCookies) { QList cookies = QNetworkCookie::parseCookies(variant.toByteArray()); - for (QNetworkCookie cookie : cookies) - { + for (QNetworkCookie cookie : cookies) { cookie.setDomain(m_url); cookie.setPath("/"); m_cookies.append(cookie); } } - if (m_cookieJar != nullptr) + if (m_cookieJar != nullptr) { resetCookieJar(); + } // Tag database delete m_tagDatabase; m_tagDatabase = TagDatabaseFactory::Create(siteDir); + m_tagDatabase->open(); m_tagDatabase->loadTypes(); } @@ -154,13 +174,15 @@ Site::~Site() void Site::resetCookieJar() { // Delete cookie jar if necessary - if (m_cookieJar != nullptr) - { m_cookieJar->deleteLater(); } + if (m_cookieJar != nullptr) { + m_cookieJar->deleteLater(); + } m_cookieJar = new QNetworkCookieJar(m_manager); - for (const QNetworkCookie &cookie : qAsConst(m_cookies)) - { m_cookieJar->insertCookie(cookie); } + for (const QNetworkCookie &cookie : qAsConst(m_cookies)) { + m_cookieJar->insertCookie(cookie); + } m_manager->setCookieJar(m_cookieJar); m_loggedIn = LoginStatus::Unknown; @@ -174,17 +196,16 @@ void Site::resetCookieJar() */ void Site::login(bool force) { - if (!force && m_loggedIn == LoginStatus::Pending) + if (!force && m_loggedIn == LoginStatus::Pending) { return; + } - if (!force && m_loggedIn != LoginStatus::Unknown) - { + if (!force && m_loggedIn != LoginStatus::Unknown) { emit loggedIn(this, LoginResult::Already); return; } - if (!m_login->isTestable()) - { + if (!canTestLogin()) { emit loggedIn(this, LoginResult::Impossible); return; } @@ -192,8 +213,9 @@ void Site::login(bool force) log(QStringLiteral("[%1] Logging in...").arg(m_url), Logger::Info); // Clear cookies if we want to force a re-login - if (force) + if (force) { resetCookieJar(); + } m_loggedIn = LoginStatus::Pending; @@ -203,7 +225,7 @@ void Site::login(bool force) bool Site::canTestLogin() const { - return m_login != nullptr && m_login->isTestable(); + return m_auth != nullptr && m_login != nullptr && m_login->isTestable(); } /** @@ -221,41 +243,48 @@ void Site::loginFinished(Login::Result result) QNetworkRequest Site::makeRequest(QUrl url, Page *page, const QString &ref, Image *img) { - if (m_autoLogin && m_loggedIn == LoginStatus::Unknown) + if (m_autoLogin && m_loggedIn == LoginStatus::Unknown) { login(); + } // Force HTTPS if set so in the settings (no mixed content allowed) - if (m_settings->value("ssl", false).toBool() && url.scheme() == "http" && url.toString().contains(m_url)) + if (m_settings->value("ssl", false).toBool() && url.scheme() == "http" && url.toString().contains(m_url)) { url.setScheme("https"); + } QNetworkRequest request(url); QString referer = m_settings->value("referer" + (!ref.isEmpty() ? "_" + ref : QString())).toString(); - if (referer.isEmpty() && !ref.isEmpty()) - { referer = m_settings->value("referer", "none").toString(); } - if (referer != "none" && (referer != "page" || page != nullptr)) - { + if (referer.isEmpty() && !ref.isEmpty()) { + referer = m_settings->value("referer", "none").toString(); + } + if (referer != "none" && (referer != "page" || page != nullptr)) { QString refHeader; - if (referer == "host") - { refHeader = url.scheme() + "://" + url.host(); } - else if (referer == "image") - { refHeader = fixUrl(url).toString(); } - else if (referer == "page" && page != nullptr) - { refHeader = fixUrl(page->url()).toString(); } - else if (referer == "details" && img != nullptr) - { refHeader = fixUrl(img->pageUrl()).toString(); } + if (referer == "host") { + refHeader = url.scheme() + "://" + url.host(); + } else if (referer == "image") { + refHeader = fixUrl(url).toString(); + } else if (referer == "page" && page != nullptr) { + refHeader = fixUrl(page->url()).toString(); + } else if (referer == "details" && img != nullptr) { + refHeader = fixUrl(img->pageUrl()).toString(); + } request.setRawHeader("Referer", refHeader.toLatin1()); } - m_login->complementRequest(&request); + if (m_login != nullptr) { + m_login->complementRequest(&request); + } QMap headers = m_settings->value("headers").toMap(); - for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) - { request.setRawHeader(it.key().toLatin1(), it.value().toString().toLatin1()); } + for (auto it = headers.constBegin(); it != headers.constEnd(); ++it) { + request.setRawHeader(it.key().toLatin1(), it.value().toString().toLatin1()); + } // User-Agent header tokens and default value QString userAgent = request.rawHeader("User-Agent"); - if (userAgent.isEmpty()) + if (userAgent.isEmpty()) { userAgent = QStringLiteral("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:52.0) Gecko/20100101 Firefox/52.0 Grabber/%version%"); + } userAgent.replace("%version%", QString(VERSION)); request.setRawHeader("User-Agent", userAgent.toLatin1()); @@ -265,8 +294,9 @@ QNetworkRequest Site::makeRequest(QUrl url, Page *page, const QString &ref, Imag int Site::msToRequest(QueryType type) const { - if (!m_lastRequest.isValid()) + if (!m_lastRequest.isValid()) { return 0; + } const qint64 sinceLastRequest = m_lastRequest.msecsTo(QDateTime::currentDateTime()); @@ -291,38 +321,6 @@ QNetworkReply *Site::getRequest(const QNetworkRequest &request) } -void Site::loadTags(int page, int limit) -{ - const QString protocol = (m_settings->value("ssl", false).toBool() ? QStringLiteral("https") : QStringLiteral("http")); - m_tagsReply = get(QUrl(protocol + "://" + m_url + "/tags.json?search[hide_empty]=yes&limit=" + QString::number(limit) + "&page=" + QString::number(page))); - connect(m_tagsReply, &QNetworkReply::finished, this, &Site::finishedTags); -} - -void Site::finishedTags() -{ - const QByteArray source = m_tagsReply->readAll(); - m_tagsReply->deleteLater(); - QList tags; - QJsonDocument src = QJsonDocument::fromJson(source); - if (!src.isNull()) - { - QJsonArray sourc = src.array(); - tags.reserve(sourc.count()); - for (int id = 0; id < sourc.count(); id++) - { - QJsonObject sc = sourc[id].toObject(); - const int cat = sc.value("category").toInt(); - tags.append(Tag( - sc.value("name").toString(), - cat == 0 ? "general" : (cat == 1 ? "artist" : (cat == 3 ? "copyright" : "character")), - sc.value("post_count").toInt(), - sc.value("related_tags").toString().split(' ') - )); - } - } - emit finishedLoadingTags(tags); -} - QVariant Site::setting(const QString &key, const QVariant &def) const { return m_settings->value(key, def); } void Site::setSetting(const QString &key, const QVariant &value, const QVariant &def) const { m_settings->setValue(key, value, def); } void Site::syncSettings() const { m_settings->sync(); } @@ -346,62 +344,76 @@ QList Site::getLoggedInApis() const { QList ret; const bool loggedIn = isLoggedIn(true); - for (Api *api : m_apis) - if (!api->needAuth() || loggedIn) + for (Api *api : m_apis) { + if (!api->needAuth() || loggedIn) { ret.append(api); + } + } return ret; } Api *Site::firstValidApi() const { const bool loggedIn = isLoggedIn(true); - for (Api *api : m_apis) - if (!api->needAuth() || loggedIn) + for (Api *api : m_apis) { + if (!api->needAuth() || loggedIn) { return api; + } + } return nullptr; } Api *Site::detailsApi() const { - for (Api *api : m_apis) - if (api->canLoadDetails()) + for (Api *api : m_apis) { + if (api->canLoadDetails()) { return api; + } + } return nullptr; } bool Site::autoLogin() const { return m_autoLogin; } void Site::setAutoLogin(bool autoLogin) { m_autoLogin = autoLogin; } -QString Site::fixLoginUrl(QString url, const QString &loginPart) const +QString Site::fixLoginUrl(QString url) const { - return m_login->complementUrl(std::move(url), loginPart); + if (m_auth == nullptr || m_login == nullptr) { + return url; + } + + return m_login->complementUrl(std::move(url)); } +Auth *Site::getAuth() const { return m_auth; } + QUrl Site::fixUrl(const QString &url, const QUrl &old) const { - if (url.isEmpty()) + if (url.isEmpty()) { return QUrl(); + } const bool ssl = m_settings->value("ssl", false).toBool(); const QString protocol = (ssl ? QStringLiteral("https") : QStringLiteral("http")); - if (url.startsWith("//")) - { return QUrl(protocol + ":" + url); } - if (url.startsWith("/")) - { + if (url.startsWith("//")) { + return QUrl(protocol + ":" + url); + } + if (url.startsWith("/")) { const QString baseUrl = m_url.mid(m_url.indexOf('/')); const QString right = url.startsWith(baseUrl) ? url.mid(baseUrl.length()) : url; return QUrl(protocol + "://" + m_url + right); } - if (!url.startsWith("http")) - { - if (old.isValid()) - { return old.resolved(QUrl(url)); } + if (!url.startsWith("http")) { + if (old.isValid()) { + return old.resolved(QUrl(url)); + } return QUrl(protocol + "://" + m_url + "/" + url); } - if (url.startsWith("http://") && ssl && url.contains(m_url)) - { return QUrl(protocol + "://" + url.mid(7)); } + if (url.startsWith("http://") && ssl && url.contains(m_url)) { + return QUrl(protocol + "://" + url.mid(7)); + } return QUrl(url); } @@ -413,11 +425,13 @@ const QList &Site::cookies() const bool Site::isLoggedIn(bool unknown, bool pending) const { - if (unknown) + if (unknown) { return m_loggedIn != LoginStatus::LoggedOut; + } - if (pending && m_loggedIn == LoginStatus::Pending) + if (pending && m_loggedIn == LoginStatus::Pending) { return true; + } return m_loggedIn == LoginStatus::LoggedIn; } diff --git a/lib/src/models/site.h b/lib/src/models/site.h index 5b8db7958..fbdf37b3d 100644 --- a/lib/src/models/site.h +++ b/lib/src/models/site.h @@ -6,11 +6,11 @@ #include #include #include -#include #include "login/login.h" class Api; +class Auth; class CustomNetworkAccessManager; class Image; class MixedSettings; @@ -18,7 +18,6 @@ class Page; class QNetworkCookie; class QNetworkCookieJar; class QNetworkReply; -class QSettings; class Source; class Tag; class TagDatabase; @@ -84,7 +83,8 @@ class Site : public QObject bool autoLogin() const; bool isLoggedIn(bool unknown = false, bool pending = false) const; bool canTestLogin() const; - QString fixLoginUrl(QString url, const QString &loginPart = "") const; + QString fixLoginUrl(QString url) const; + Auth *getAuth() const; private: QNetworkReply *getRequest(const QNetworkRequest &request); @@ -92,8 +92,6 @@ class Site : public QObject public slots: void login(bool force = false); void loginFinished(Login::Result result); - void loadTags(int page, int limit); - void finishedTags(); protected: void resetCookieJar(); @@ -118,13 +116,12 @@ class Site : public QObject // Login Login *m_login; + Auth *m_auth; LoginStatus m_loggedIn; bool m_autoLogin; // Async - std::function m_lastCallback; QDateTime m_lastRequest; - QNetworkRequest m_callbackRequest; }; Q_DECLARE_METATYPE(Site::LoginResult) diff --git a/lib/src/models/source-guesser.cpp b/lib/src/models/source-guesser.cpp index 703ae2b0e..2f93b5f68 100644 --- a/lib/src/models/source-guesser.cpp +++ b/lib/src/models/source-guesser.cpp @@ -20,18 +20,14 @@ Source *SourceGuesser::start() m_cache.clear(); int current = 0; - for (Source *source : qAsConst(m_sources)) - { + for (Source *source : qAsConst(m_sources)) { Api *api = source->getApis().first(); - if (api->canLoadCheck()) - { + if (api->canLoadCheck()) { const QString checkUrl = api->checkUrl().url; - if (!m_cache.contains(checkUrl)) - { + if (!m_cache.contains(checkUrl)) { QUrl getUrl(m_url + checkUrl); QNetworkReply *reply; - do - { + do { reply = m_manager->get(QNetworkRequest(getUrl)); QEventLoop loop; connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); @@ -40,8 +36,7 @@ Source *SourceGuesser::start() getUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); } while (!getUrl.isEmpty()); - if (reply->error() != 0) - { + if (reply->error() != 0) { log(QStringLiteral("Error getting the test page: %1.").arg(reply->errorString()), Logger::Error); emit progress(++current); continue; @@ -50,8 +45,7 @@ Source *SourceGuesser::start() m_cache[checkUrl] = reply->readAll(); } - if (api->parseCheck(m_cache[checkUrl]).ok) - { + if (api->parseCheck(m_cache[checkUrl], 200).ok) { emit finished(source); return source; } diff --git a/lib/src/models/source.cpp b/lib/src/models/source.cpp index 96019bfd0..52fb0c383 100644 --- a/lib/src/models/source.cpp +++ b/lib/src/models/source.cpp @@ -1,8 +1,16 @@ #include "models/source.h" +#include #include #include #include +#include "auth/auth-const-field.h" +#include "auth/auth-field.h" +#include "auth/auth-hash-field.h" +#include "auth/http-auth.h" +#include "auth/oauth2-auth.h" +#include "auth/url-auth.h" #include "functions.h" +#include "logger.h" #include "models/api/api.h" #include "models/api/javascript-api.h" #include "models/api/javascript-console-helper.h" @@ -23,24 +31,23 @@ QJSEngine *Source::jsEngine() { static QJSEngine *engine = nullptr; - if (engine == nullptr) - { + if (engine == nullptr) { engine = new QJSEngine(); engine->globalObject().setProperty("Grabber", engine->newQObject(new JavascriptGrabberHelper(*engine))); engine->globalObject().setProperty("console", engine->newQObject(new JavascriptConsoleHelper("[JavaScript] ", engine))); // JavaScript helper file QFile jsHelper(m_dir + "/../helper.js"); - if (jsHelper.open(QFile::ReadOnly | QFile::Text)) - { + if (jsHelper.open(QFile::ReadOnly | QFile::Text)) { QJSValue helperResult = engine->evaluate(jsHelper.readAll(), jsHelper.fileName()); jsHelper.close(); - if (helperResult.isError()) - { log(QStringLiteral("Uncaught exception at line %1: %2").arg(helperResult.property("lineNumber").toInt()).arg(helperResult.toString()), Logger::Error); } + if (helperResult.isError()) { + log(QStringLiteral("Uncaught exception at line %1: %2").arg(helperResult.property("lineNumber").toInt()).arg(helperResult.toString()), Logger::Error); + } + } else { + log(QStringLiteral("JavaScript helper file could not be opened"), Logger::Error); } - else - { log(QStringLiteral("JavaScript helper file could not be opened"), Logger::Error); } } return engine; @@ -49,8 +56,9 @@ QMutex *Source::jsEngineMutex() { static QMutex *mutex = nullptr; - if (mutex == nullptr) - { mutex = new QMutex(); } + if (mutex == nullptr) { + mutex = new QMutex(); + } return mutex; } @@ -58,98 +66,133 @@ QMutex *Source::jsEngineMutex() Source::Source(Profile *profile, const QString &dir) : m_dir(dir), m_diskName(QFileInfo(dir).fileName()), m_profile(profile), m_updater(m_diskName, m_dir, getUpdaterBaseUrl()) { - // Load XML details for this source from its model file - QFile file(m_dir + "/model.xml"); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + // Tag format mapper + static const QMap caseAssoc { - const QString fileContents = file.readAll(); - QDomDocument doc; - QString errorMsg; - int errorLine, errorColumn; - if (!doc.setContent(fileContents, false, &errorMsg, &errorLine, &errorColumn)) - { log(QStringLiteral("Error parsing XML file: %1 (%2 - %3).").arg(errorMsg, QString::number(errorLine), QString::number(errorColumn)), Logger::Error); } - else - { - const QDomElement docElem = doc.documentElement(); - const QMap details = domToMap(docElem); - - // Tag format mapper - static const QMap caseAssoc - { - { "lower", TagNameFormat::Lower }, - { "upper_first", TagNameFormat::UpperFirst }, - { "upper", TagNameFormat::Upper }, - { "caps", TagNameFormat::Caps }, - }; - - // Javascript models - QFile js(m_dir + "/model.js"); - if (js.exists() && js.open(QIODevice::ReadOnly | QIODevice::Text)) - { - log(QStringLiteral("Using Javascript model for %1").arg(m_diskName), Logger::Debug); - - const QString src = "(function() { var window = {}; " + js.readAll().replace("export var source = ", "return ") + " })()"; - - m_jsSource = jsEngine()->evaluate(src, js.fileName()); - if (m_jsSource.isError()) - { log(QStringLiteral("Uncaught exception at line %1: %2").arg(m_jsSource.property("lineNumber").toInt()).arg(m_jsSource.toString()), Logger::Error); } - else - { - m_name = m_jsSource.property("name").toString(); - - // Get the list of APIs for this Source - const QJSValue apis = m_jsSource.property("apis"); - QJSValueIterator it(apis); - while (it.hasNext()) - { - it.next(); - m_apis.append(new JavascriptApi(details, m_jsSource, jsEngineMutex(), it.name())); + { "lower", TagNameFormat::Lower }, + { "upper_first", TagNameFormat::UpperFirst }, + { "upper", TagNameFormat::Upper }, + { "caps", TagNameFormat::Caps }, + }; + + // Javascript models + QFile js(m_dir + "/model.js"); + if (js.exists() && js.open(QIODevice::ReadOnly | QIODevice::Text)) { + log(QStringLiteral("Using Javascript model for %1").arg(m_diskName), Logger::Debug); + + const QString src = "(function() { var window = {}; " + js.readAll().replace("export var source = ", "return ") + " })()"; + + m_jsSource = jsEngine()->evaluate(src, js.fileName()); + if (m_jsSource.isError()) { + log(QStringLiteral("Uncaught exception at line %1: %2").arg(m_jsSource.property("lineNumber").toInt()).arg(m_jsSource.toString()), Logger::Error); + } else { + m_name = m_jsSource.property("name").toString(); + m_additionalTokens = jsToStringList(m_jsSource.property("tokens")); + + // Get the list of APIs for this Source + const QJSValue apis = m_jsSource.property("apis"); + QJSValueIterator it(apis); + while (it.hasNext()) { + it.next(); + m_apis.append(new JavascriptApi(m_jsSource, jsEngineMutex(), it.name())); + } + if (m_apis.isEmpty()) { + log(QStringLiteral("No valid source has been found in the model.js file from %1.").arg(m_name)); + } + + // Read tag naming format + const QJSValue &tagFormat = m_jsSource.property("tagFormat"); + if (!tagFormat.isUndefined()) { + const auto caseFormat = caseAssoc.value(tagFormat.property("case").toString(), TagNameFormat::Lower); + m_tagNameFormat = TagNameFormat(caseFormat, tagFormat.property("wordSeparator").toString()); + } + + // Read auth information + const QJSValue auths = m_jsSource.property("auth"); + QJSValueIterator authIt(auths); + while (authIt.hasNext()) { + authIt.next(); + + const QString &id = authIt.name(); + const QJSValue &auth = authIt.value(); + + const QString type = auth.property("type").toString(); + Auth *ret = nullptr; + + const QJSValue check = auth.property("check"); + const QString checkType = check.isObject() ? check.property("type").toString() : QString(); + + if (type == "oauth2") { + const QString authType = auth.property("authType").toString(); + const QString tokenUrl = auth.property("tokenUrl").toString(); + ret = new OAuth2Auth(type, authType, tokenUrl); + } else { + QList fields; + const QJSValue &jsFields = auth.property("fields"); + const quint32 length = jsFields.property("length").toUInt(); + for (quint32 i = 0; i < length; ++i) { + const QJSValue &field = jsFields.property(i); + + const QString fid = !field.property("id").isUndefined() ? field.property("id").toString() : QString(); + const QString key = !field.property("key").isUndefined() ? field.property("key").toString() : QString(); + const QString type = field.property("type").toString(); + + if (type == "hash") { + const QString algoStr = field.property("hash").toString(); + const auto algo = algoStr == "sha1" ? QCryptographicHash::Sha1 : QCryptographicHash::Md5; + fields.append(new AuthHashField(key, algo, field.property("salt").toString())); + } else if (type == "const") { + const QString value = field.property("value").toString(); + fields.append(new AuthConstField(key, value)); + } else { + fields.append(new AuthField(fid, key, type == "password" ? AuthField::Password : AuthField::Text)); + } } - if (m_apis.isEmpty()) - { log(QStringLiteral("No valid source has been found in the model.js file from %1.").arg(m_name)); } - - // Read tag naming format - const QJSValue &tagFormat = m_jsSource.property("tagFormat"); - if (!tagFormat.isUndefined()) - { - const auto caseFormat = caseAssoc.value(tagFormat.property("case").toString(), TagNameFormat::Lower); - m_tagNameFormat = TagNameFormat(caseFormat, tagFormat.property("wordSeparator").toString()); + + if (type == "get" || type == "post") { + const QString url = auth.property("url").toString(); + const QString cookie = checkType == "cookie" ? check.property("key").toString() : QString(); + ret = new HttpAuth(type, url, fields, cookie); + } else { + const int maxPage = checkType == "max_page" ? check.property("value").toInt() : 0; + ret = new UrlAuth(type, fields, maxPage); } } - js.close(); + if (ret != nullptr) { + m_auths.insert(id, ret); + } } - else - { log(QStringLiteral("Javascript model not found for %1").arg(m_diskName), Logger::Warning); } } - file.close(); + js.close(); + } else { + log(QStringLiteral("Javascript model not found for %1").arg(m_diskName), Logger::Warning); } - else - { log(QStringLiteral("Impossible to open the model file '%1'").arg(m_dir + "/model.xml"), Logger::Warning); } // Get the list of all sites pertaining to this source QFile f(m_dir + "/sites.txt"); - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) - { - while (!f.atEnd()) - { + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + while (!f.atEnd()) { QString line = f.readLine().trimmed(); - if (line.isEmpty()) + if (line.isEmpty()) { continue; + } auto site = new Site(line, this); m_sites.append(site); } } - if (m_sites.isEmpty()) - { log(QStringLiteral("No site for source %1").arg(m_name), Logger::Debug); } + if (m_sites.isEmpty()) { + log(QStringLiteral("No site for source %1").arg(m_name), Logger::Debug); + } } Source::~Source() { qDeleteAll(m_apis); qDeleteAll(m_sites); + qDeleteAll(m_auths); } @@ -159,11 +202,16 @@ const QList &Source::getSites() const { return m_sites; } const QList &Source::getApis() const { return m_apis; } Profile *Source::getProfile() const { return m_profile; } const SourceUpdater &Source::getUpdater() const { return m_updater; } +const QStringList &Source::getAdditionalTokens() const { return m_additionalTokens; } +const QMap &Source::getAuths() const { return m_auths; } +Auth *Source::getAuth(const QString &name) const { return m_auths.value(name); } Api *Source::getApi(const QString &name) const { - for (Api *api : this->getApis()) - if (api->getName() == name) + for (Api *api : this->getApis()) { + if (api->getName() == name) { return api; + } + } return nullptr; } diff --git a/lib/src/models/source.h b/lib/src/models/source.h index 3263f69c6..dc3de4f49 100644 --- a/lib/src/models/source.h +++ b/lib/src/models/source.h @@ -3,8 +3,10 @@ #include #include +#include #include #include +#include "auth/auth.h" #include "tags/tag-name-format.h" #include "updater/source-updater.h" @@ -29,8 +31,11 @@ class Source : public QObject const QList &getSites() const; const QList &getApis() const; Api *getApi(const QString &name) const; + const QMap &getAuths() const; + Auth *getAuth(const QString &name) const; Profile *getProfile() const; const SourceUpdater &getUpdater() const; + const QStringList &getAdditionalTokens() const; protected: QJSEngine *jsEngine(); @@ -42,6 +47,8 @@ class Source : public QObject QString m_name; QList m_sites; QList m_apis; + QMap m_auths; + QStringList m_additionalTokens; Profile *m_profile; SourceUpdater m_updater; TagNameFormat m_tagNameFormat; diff --git a/lib/src/pch.cpp b/lib/src/pch.cpp deleted file mode 100644 index 2cd79060c..000000000 --- a/lib/src/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "src/pch.h" diff --git a/lib/src/pch.h b/lib/src/pch.h deleted file mode 100644 index e2423d645..000000000 --- a/lib/src/pch.h +++ /dev/null @@ -1,8 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include diff --git a/lib/src/reverse-search/reverse-search-engine.cpp b/lib/src/reverse-search/reverse-search-engine.cpp index ad5ec18b9..c01067b21 100644 --- a/lib/src/reverse-search/reverse-search-engine.cpp +++ b/lib/src/reverse-search/reverse-search-engine.cpp @@ -10,20 +10,20 @@ ReverseSearchEngine::ReverseSearchEngine(int id, const QString &icon, QString na QIcon ReverseSearchEngine::loadIcon(const QString &path) const { - if (path.isEmpty()) + if (path.isEmpty()) { return QIcon(); + } QFile f(path); - if (f.open(QFile::ReadOnly)) - { + if (f.open(QFile::ReadOnly)) { QByteArray data12 = f.read(12); f.close(); - if (data12.length() >= 12) - { + if (data12.length() >= 12) { QString ext = getExtensionFromHeader(data12); - if (!ext.isEmpty()) + if (!ext.isEmpty()) { return QIcon(QPixmap(path, ext.toStdString().c_str())); + } } } diff --git a/lib/src/reverse-search/reverse-search-loader.cpp b/lib/src/reverse-search/reverse-search-loader.cpp index 1c26e4db4..b49bc1867 100644 --- a/lib/src/reverse-search/reverse-search-loader.cpp +++ b/lib/src/reverse-search/reverse-search-loader.cpp @@ -13,8 +13,7 @@ QList ReverseSearchLoader::getAllReverseSearchEngines() con QMap ret; // Default groups - if (!m_settings->childGroups().contains("WebServices")) - { + if (!m_settings->childGroups().contains("WebServices")) { ret.insert(1, ReverseSearchEngine(1, savePath("webservices/1.ico"), "IQDB", "https://iqdb.org/?url={url}", 1)); ret.insert(2, ReverseSearchEngine(2, savePath("webservices/2.ico"), "SauceNAO", "https://saucenao.com/search.php?db=999&url={url}", 2)); ret.insert(3, ReverseSearchEngine(3, savePath("webservices/3.ico"), "Google", "https://www.google.com/searchbyimage?image_url={url}", 3)); @@ -26,8 +25,7 @@ QList ReverseSearchLoader::getAllReverseSearchEngines() con // Load groups m_settings->beginGroup("WebServices"); const QStringList &webGroups = m_settings->childGroups(); - for (const QString &group : webGroups) - { + for (const QString &group : webGroups) { m_settings->beginGroup(group); const int id = group.toInt(); const int order = m_settings->value("order").toInt(); diff --git a/lib/src/search/ast/search-node-tag.h b/lib/src/search/ast/search-node-tag.h index 9b3c9982e..25fe5beff 100644 --- a/lib/src/search/ast/search-node-tag.h +++ b/lib/src/search/ast/search-node-tag.h @@ -9,7 +9,7 @@ struct SearchNodeTag : public SearchNode { Tag tag; - SearchNodeTag(Tag tag); + explicit SearchNodeTag(Tag tag); void accept(SearchVisitor &v) const override; }; diff --git a/lib/src/search/search-format-visitor.cpp b/lib/src/search/search-format-visitor.cpp index 7ec8347a0..f117aed44 100644 --- a/lib/src/search/search-format-visitor.cpp +++ b/lib/src/search/search-format-visitor.cpp @@ -1,7 +1,7 @@ #include "search/search-format-visitor.h" -#include "search/ast/search-node.h" #include "search/ast/search-node-op.h" #include "search/ast/search-node-tag.h" +#include "search/ast/search-node.h" SearchFormatVisitor::SearchFormatVisitor(SearchFormat searchFormat) diff --git a/lib/src/search/search-format-visitor.h b/lib/src/search/search-format-visitor.h index 97c9b9c4d..eee2c78a7 100644 --- a/lib/src/search/search-format-visitor.h +++ b/lib/src/search/search-format-visitor.h @@ -3,7 +3,6 @@ #include "search/ast/search-visitor.h" #include "search/search-format.h" -#include "tags/tag-name-format.h" struct SearchNode; @@ -11,7 +10,7 @@ struct SearchNode; class SearchFormatVisitor : public SearchVisitor { public: - SearchFormatVisitor(SearchFormat searchFormat); + explicit SearchFormatVisitor(SearchFormat searchFormat); QString run(const SearchNode &node); QString error() const; @@ -23,7 +22,7 @@ class SearchFormatVisitor : public SearchVisitor QString m_result; QString m_error; - bool m_inPrecedent; + bool m_inPrecedent = false; }; #endif // SEARCH_FORMAT_VISITOR_H diff --git a/lib/src/secure-file.cpp b/lib/src/secure-file.cpp index 6c12bada5..8bc27675c 100644 --- a/lib/src/secure-file.cpp +++ b/lib/src/secure-file.cpp @@ -22,8 +22,7 @@ quint64 SecureFile::generateIntKey(const QString &key) const void SecureFile::write(const QByteArray &data) { - if (m_file.open(QFile::WriteOnly)) - { + if (m_file.open(QFile::WriteOnly)) { m_file.write(m_encryptor.encryptToByteArray(data)); m_file.close(); } @@ -32,8 +31,7 @@ void SecureFile::write(const QByteArray &data) QByteArray SecureFile::readAll() { QByteArray data; - if (m_file.open(QFile::ReadOnly)) - { + if (m_file.open(QFile::ReadOnly)) { data = m_encryptor.decryptToByteArray(m_file.readAll()); m_file.close(); } diff --git a/lib/src/tags/tag-api.cpp b/lib/src/tags/tag-api.cpp index d0b0ee16d..4056415ef 100755 --- a/lib/src/tags/tag-api.cpp +++ b/lib/src/tags/tag-api.cpp @@ -15,28 +15,30 @@ TagApi::TagApi(Profile *profile, Site *site, Api *api, int page, int limit, QObj TagApi::~TagApi() { - if (m_reply != nullptr) + if (m_reply != nullptr) { m_reply->deleteLater(); + } } void TagApi::load(bool rateLimit) { // Load the request with a possible delay int ms = m_site->msToRequest(rateLimit ? Site::QueryType::Retry : Site::QueryType::List); - if (ms > 0) - { QTimer::singleShot(ms, this, SLOT(loadNow())); } - else - { loadNow(); } + if (ms > 0) { + QTimer::singleShot(ms, this, SLOT(loadNow())); + } else { + loadNow(); + } } void TagApi::loadNow() { log(QStringLiteral("[%1] Loading tags page `%2`").arg(m_site->url(), m_url.toString().toHtmlEscaped()), Logger::Info); - if (m_reply != nullptr) - { - if (m_reply->isRunning()) + if (m_reply != nullptr) { + if (m_reply->isRunning()) { m_reply->abort(); + } m_reply->deleteLater(); } @@ -47,8 +49,9 @@ void TagApi::loadNow() void TagApi::abort() { - if (m_reply != nullptr && m_reply->isRunning()) + if (m_reply != nullptr && m_reply->isRunning()) { m_reply->abort(); + } } void TagApi::parse() @@ -57,8 +60,7 @@ void TagApi::parse() // Check redirection QUrl redirection = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redirection.isEmpty()) - { + if (!redirection.isEmpty()) { QUrl newUrl = m_site->fixUrl(redirection.toString(), m_url); log(QStringLiteral("[%1] Redirecting tags page `%2` to `%3`").arg(m_site->url(), m_url.toString().toHtmlEscaped(), newUrl.toString().toHtmlEscaped()), Logger::Info); m_url = newUrl; @@ -68,18 +70,18 @@ void TagApi::parse() // Try to read the reply QString source = m_reply->readAll(); - if (source.isEmpty()) - { - if (m_reply->error() != QNetworkReply::OperationCanceledError) - { log(QStringLiteral("[%1][%2] Loading error: %3 (%4)").arg(m_site->url(), m_api->getName(), m_reply->errorString()).arg(m_reply->error()), Logger::Error); } + if (source.isEmpty()) { + if (m_reply->error() != QNetworkReply::OperationCanceledError) { + log(QStringLiteral("[%1][%2] Loading error: %3 (%4)").arg(m_site->url(), m_api->getName(), m_reply->errorString()).arg(m_reply->error()), Logger::Error); + } emit finishedLoading(this, LoadResult::Error); return; } // Parse source - ParsedTags ret = m_api->parseTags(source, m_site); - if (!ret.error.isEmpty()) - { + const int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + ParsedTags ret = m_api->parseTags(source, statusCode, m_site); + if (!ret.error.isEmpty()) { log(QStringLiteral("[%1][%2] %3").arg(m_site->url(), m_api->getName(), ret.error), Logger::Warning); emit finishedLoading(this, LoadResult::Error); return; diff --git a/lib/src/tags/tag-database-factory.cpp b/lib/src/tags/tag-database-factory.cpp index 984eb792d..fcdb2b5af 100644 --- a/lib/src/tags/tag-database-factory.cpp +++ b/lib/src/tags/tag-database-factory.cpp @@ -6,13 +6,15 @@ TagDatabase *TagDatabaseFactory::Create(QString directory) { - if (!directory.endsWith("/") && !directory.endsWith("\\")) + if (!directory.endsWith("/") && !directory.endsWith("\\")) { directory += "/"; + } const QString typesFile = directory + "tag-types.txt"; - if (QFile::exists(directory + "tags.db")) + if (QFile::exists(directory + "tags.db")) { return new TagDatabaseSqlite(typesFile, directory + "tags.db"); + } return new TagDatabaseInMemory(typesFile, directory + "tags.txt"); } diff --git a/lib/src/tags/tag-database-in-memory.cpp b/lib/src/tags/tag-database-in-memory.cpp index db0b0b6b8..47a46141d 100644 --- a/lib/src/tags/tag-database-in-memory.cpp +++ b/lib/src/tags/tag-database-in-memory.cpp @@ -5,41 +5,47 @@ TagDatabaseInMemory::TagDatabaseInMemory(const QString &typeFile, QString tagFile) - : TagDatabase(typeFile), m_tagFile(std::move(tagFile)) + : TagDatabase(typeFile), m_tagFile(std::move(tagFile)), m_count(-1) {} bool TagDatabaseInMemory::load() { // Don't reload databases - if (!m_database.isEmpty()) + if (!m_database.isEmpty()) { return true; + } // Load tag types - if (!TagDatabase::load()) + if (!TagDatabase::load()) { return false; + } QFile file(m_tagFile); - if (!file.exists()) + if (!file.exists()) { return true; - if (!file.open(QFile::ReadOnly | QFile::Text)) + } + if (!file.open(QFile::ReadOnly | QFile::Text)) { return false; + } QTextStream in(&file); - while (!in.atEnd()) - { + while (!in.atEnd()) { QString line = in.readLine(); QStringList data = line.split(','); - if (data.count() != 2) + if (data.count() != 2) { continue; + } int tId = data[1].toInt(); - if (!m_tagTypes.contains(tId)) + if (!m_tagTypes.contains(tId)) { continue; + } QString tag = data[0]; - if (tag.isEmpty()) + if (tag.isEmpty()) { continue; + } tag.squeeze(); m_database.insert(tag, m_tagTypes[tId]); @@ -53,17 +59,18 @@ bool TagDatabaseInMemory::load() bool TagDatabaseInMemory::save() { QFile file(m_tagFile); - if (!file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) + if (!file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { return false; + } // Inverted tag type map to get the tag type ID from its name QMap tagTypes; - for (auto it = m_tagTypes.constBegin(); it != m_tagTypes.constEnd(); ++it) + for (auto it = m_tagTypes.constBegin(); it != m_tagTypes.constEnd(); ++it) { tagTypes.insert(it.value().name(), it.key()); + } QHashIterator i(m_database); - while (i.hasNext()) - { + while (i.hasNext()) { i.next(); TagType tagType = i.value(); @@ -79,21 +86,46 @@ bool TagDatabaseInMemory::save() void TagDatabaseInMemory::setTags(const QList &tags) { m_database.clear(); - for (const Tag &tag : tags) + for (const Tag &tag : tags) { m_database.insert(tag.text(), tag.type()); + } } QMap TagDatabaseInMemory::getTagTypes(const QStringList &tags) const { QMap ret; - for (const QString &tag : tags) - if (m_database.contains(tag)) + for (const QString &tag : tags) { + if (m_database.contains(tag)) { ret.insert(tag, m_database[tag]); + } + } return ret; } int TagDatabaseInMemory::count() const { - return m_database.count(); + if (!m_database.isEmpty()) { + return m_database.count(); + } + + if (m_count != -1) { + return m_count; + } + + m_count = 0; + + QFile file(m_tagFile); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return m_count; + } + + QTextStream in(&file); + while (!in.atEnd()) { + m_count++; + in.readLine(); + } + file.close(); + + return m_count; } diff --git a/lib/src/tags/tag-database-in-memory.h b/lib/src/tags/tag-database-in-memory.h index 819ef09f5..7099f9ffb 100644 --- a/lib/src/tags/tag-database-in-memory.h +++ b/lib/src/tags/tag-database-in-memory.h @@ -25,6 +25,7 @@ class TagDatabaseInMemory : public TagDatabase private: QString m_tagFile; QHash m_database; + mutable int m_count; }; #endif // TAG_DATABASE_IN_MEMORY_H diff --git a/lib/src/tags/tag-database-sqlite.cpp b/lib/src/tags/tag-database-sqlite.cpp index 362911fee..2e55290b1 100644 --- a/lib/src/tags/tag-database-sqlite.cpp +++ b/lib/src/tags/tag-database-sqlite.cpp @@ -13,21 +13,17 @@ TagDatabaseSqlite::TagDatabaseSqlite(const QString &typeFile, QString tagFile) : TagDatabase(typeFile), m_tagFile(std::move(tagFile)), m_count(-1) {} -bool TagDatabaseSqlite::load() +bool TagDatabaseSqlite::open() { - // Don't reload databases - if (m_database.isOpen()) + // Don't re-open databases + if (m_database.isOpen()) { return true; - - // Load tag types - if (!TagDatabase::load()) - return false; + } // Load and connect to the database m_database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); m_database.setDatabaseName(m_tagFile); - if (!m_database.open()) - { + if (!m_database.open()) { log(QStringLiteral("Could not open tag database: %1").arg(m_database.lastError().text()), Logger::Error); return false; } @@ -35,13 +31,24 @@ bool TagDatabaseSqlite::load() // Create schema if necessary QSqlQuery createQuery(m_database); createQuery.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS tags (id INT, tag VARCHAR(255), ttype INT);")); - if (!createQuery.exec()) - { + if (!createQuery.exec()) { log(QStringLiteral("Could not create tag database schema: %1").arg(createQuery.lastError().text()), Logger::Error); return false; } - return true; + return TagDatabase::open(); +} + +bool TagDatabaseSqlite::close() +{ + m_database.close(); + + return TagDatabase::close(); +} + +bool TagDatabaseSqlite::load() +{ + return TagDatabase::load(); } bool TagDatabaseSqlite::save() @@ -53,38 +60,38 @@ void TagDatabaseSqlite::setTags(const QList &tags) { // Inverted tag type map to get the tag type ID from its name QMap tagTypes; - for (auto it = m_tagTypes.constBegin(); it != m_tagTypes.constEnd(); ++it) + for (auto it = m_tagTypes.constBegin(); it != m_tagTypes.constEnd(); ++it) { tagTypes.insert(it.value().name(), it.key()); + } QSqlQuery clearQuery(m_database); clearQuery.prepare(QStringLiteral("DELETE FROM tags")); - if (!clearQuery.exec()) - { + if (!clearQuery.exec()) { log(QStringLiteral("SQL error when clearing tags: %1").arg(clearQuery.lastError().text()), Logger::Error); return; } - if (!m_database.transaction()) + if (!m_database.transaction()) { return; + } QSqlQuery addQuery(m_database); addQuery.prepare(QStringLiteral("INSERT INTO tags (id, tag, ttype) VALUES (:id, :tag, :ttype)")); - for (const Tag &tag : tags) - { + for (const Tag &tag : tags) { const QString &type = tag.type().name(); addQuery.bindValue(":id", tag.id()); addQuery.bindValue(":tag", tag.text()); addQuery.bindValue(":ttype", tagTypes.contains(type) ? tagTypes[type] : -1); - if (!addQuery.exec()) - { + if (!addQuery.exec()) { log(QStringLiteral("SQL error when adding tag: %1").arg(addQuery.lastError().text()), Logger::Error); return; } } - if (!m_database.commit()) + if (!m_database.commit()) { return; + } m_count = -1; } @@ -96,12 +103,10 @@ QMap TagDatabaseSqlite::getTagTypes(const QStringList &tags) c // Escape values QStringList formatted; QSqlDriver *driver = m_database.driver(); - for (const QString &tag : tags) - { - if (m_cache.contains(tag)) - { ret.insert(tag, m_cache[tag]); } - else - { + for (const QString &tag : tags) { + if (m_cache.contains(tag)) { + ret.insert(tag, m_cache[tag]); + } else { QSqlField f; f.setType(QVariant::String); f.setValue(tag); @@ -110,23 +115,22 @@ QMap TagDatabaseSqlite::getTagTypes(const QStringList &tags) c } // If all values have already been loaded from the memory cache - if (formatted.isEmpty()) + if (formatted.isEmpty()) { return ret; + } // Execute query const QString sql = "SELECT tag, ttype FROM tags WHERE tag IN (" + formatted.join(",") + ")"; QSqlQuery query(m_database); query.setForwardOnly(true); - if (!query.exec(sql)) - { + if (!query.exec(sql)) { log(QStringLiteral("SQL error when getting tags: %1").arg(query.lastError().text()), Logger::Error); return ret; } const int idTag = query.record().indexOf("tag"); const int idTtype = query.record().indexOf("ttype"); - while (query.next()) - { + while (query.next()) { const QString tag = query.value(idTag).toString(); const TagType type = m_tagTypes[query.value(idTtype).toInt()]; ret.insert(tag, type); @@ -138,13 +142,13 @@ QMap TagDatabaseSqlite::getTagTypes(const QStringList &tags) c int TagDatabaseSqlite::count() const { - if (m_count != -1) + if (m_count != -1) { return m_count; + } QSqlQuery query(m_database); const QString sql = QStringLiteral("SELECT COUNT(*) FROM tags"); - if (!query.exec(sql) || !query.next()) - { + if (!query.exec(sql) || !query.next()) { log(QStringLiteral("SQL error when getting tag count: %1").arg(query.lastError().text()), Logger::Error); return -1; } diff --git a/lib/src/tags/tag-database-sqlite.h b/lib/src/tags/tag-database-sqlite.h index 355e355a0..6b4cf7792 100644 --- a/lib/src/tags/tag-database-sqlite.h +++ b/lib/src/tags/tag-database-sqlite.h @@ -14,6 +14,8 @@ class TagDatabaseSqlite : public TagDatabase public: TagDatabaseSqlite(const QString &typeFile, QString tagFile); ~TagDatabaseSqlite() override = default; + bool open() override; + bool close() override; bool load() override; bool save() override; void setTags(const QList &tags) override; diff --git a/lib/src/tags/tag-database.cpp b/lib/src/tags/tag-database.cpp index 523399b13..0366ce16c 100644 --- a/lib/src/tags/tag-database.cpp +++ b/lib/src/tags/tag-database.cpp @@ -9,33 +9,53 @@ TagDatabase::TagDatabase(QString typeFile) : m_typeFile(std::move(typeFile)) {} -bool TagDatabase::load() +bool TagDatabase::open() +{ + m_isOpen = true; + return true; +} + +bool TagDatabase::isOpen() const +{ + return m_isOpen; +} + +bool TagDatabase::close() { - loadTypes(); + m_isOpen = false; return true; } -void TagDatabase::loadTypes() +bool TagDatabase::load() +{ + return loadTypes(); +} + +bool TagDatabase::loadTypes() { - if (!m_tagTypes.isEmpty()) - return; + if (!m_tagTypes.isEmpty()) { + return true; + } QFile f(m_typeFile); - if (!f.open(QFile::ReadOnly | QFile::Text)) - return; + if (!f.open(QFile::ReadOnly | QFile::Text)) { + return false; + } QTextStream in(&f); - while (!in.atEnd()) - { + while (!in.atEnd()) { QString line = in.readLine(); QStringList data = line.split(','); - if (data.count() != 2) + if (data.count() != 2) { continue; + } m_tagTypes.insert(data[0].toInt(), TagType(data[1])); } f.close(); + + return true; } const QMap &TagDatabase::tagTypes() const diff --git a/lib/src/tags/tag-database.h b/lib/src/tags/tag-database.h index ceed80ead..f63bb7efa 100644 --- a/lib/src/tags/tag-database.h +++ b/lib/src/tags/tag-database.h @@ -12,7 +12,10 @@ class TagDatabase { public: virtual ~TagDatabase() = default; - void loadTypes(); + bool loadTypes(); + virtual bool open(); + bool isOpen() const; + virtual bool close(); virtual bool load(); virtual bool save() = 0; virtual void setTags(const QList &tags) = 0; @@ -25,6 +28,7 @@ class TagDatabase protected: QMap m_tagTypes; + bool m_isOpen = false; private: QString m_typeFile; diff --git a/lib/src/tags/tag-name-format.cpp b/lib/src/tags/tag-name-format.cpp index a88ace31e..3c9654f83 100644 --- a/lib/src/tags/tag-name-format.cpp +++ b/lib/src/tags/tag-name-format.cpp @@ -25,8 +25,9 @@ QString TagNameFormat::formatted(const QStringList &words) const { QStringList res; res.reserve(words.count()); - for (int i = 0; i < words.length(); ++i) + for (int i = 0; i < words.length(); ++i) { res.append(formatted(words[i], i)); + } return res.join(m_wordSeparator); } @@ -42,8 +43,9 @@ QString TagNameFormat::formatted(const QString &word, int index) const case TagNameFormat::UpperFirst: { auto res = word.toLower(); - if (index == 0 || m_caseFormat == TagNameFormat::Upper) + if (index == 0 || m_caseFormat == TagNameFormat::Upper) { res[0] = res[0].toUpper(); + } return res; } diff --git a/lib/src/tags/tag-name.cpp b/lib/src/tags/tag-name.cpp index 492eca046..996c4fb68 100644 --- a/lib/src/tags/tag-name.cpp +++ b/lib/src/tags/tag-name.cpp @@ -14,11 +14,13 @@ QString TagName::normalized() const QString TagName::formatted(const TagNameFormat &format) const { - if (format == m_format) + if (format == m_format) { return m_name; + } - if (m_words.isEmpty()) + if (m_words.isEmpty()) { m_words = m_name.split(m_format.wordSeparator()); + } return format.formatted(m_words); } diff --git a/lib/src/tags/tag-stylist.cpp b/lib/src/tags/tag-stylist.cpp index c991a25b0..df0a6ab3b 100644 --- a/lib/src/tags/tag-stylist.cpp +++ b/lib/src/tags/tag-stylist.cpp @@ -1,6 +1,7 @@ #include "tags/tag-stylist.h" #include #include +#include #include #include "functions.h" #include "models/favorite.h" @@ -15,29 +16,40 @@ TagStylist::TagStylist(Profile *profile) QStringList TagStylist::stylished(QList tags, bool count, bool noUnderscores, const QString &sort) const { // Sort tag list - if (sort == QLatin1String("type")) + if (sort == QLatin1String("type")) { std::sort(tags.begin(), tags.end(), sortTagsByType); - else if (sort == QLatin1String("name")) + } else if (sort == QLatin1String("name")) { std::sort(tags.begin(), tags.end(), sortTagsByName); - else if (sort == QLatin1String("count")) + } else if (sort == QLatin1String("count")) { std::sort(tags.begin(), tags.end(), sortTagsByCount); + } // Generate style map static const QStringList tlist = QStringList() << "artists" << "circles" << "copyrights" << "characters" << "species" << "metas" << "models" << "generals" << "favorites" << "keptForLater" << "blacklisteds" << "ignoreds" << "favorites"; static const QStringList defaults = QStringList() << "#aa0000" << "#55bbff" << "#aa00aa" << "#00aa00" << "#ee6600" << "#ee6600" << "#0000ee" << "#000000" << "#ffc0cb" << "#000000" << "#000000" << "#999999" << "#ffcccc"; QMap styles; - for (const QString &key : tlist) - { - QFont font; - font.fromString(m_profile->getSettings()->value("Coloring/Fonts/" + key).toString()); + for (const QString &key : tlist) { const QString color = m_profile->getSettings()->value("Coloring/Colors/" + key, defaults.at(tlist.indexOf(key))).toString(); - styles.insert(key, "color:" + color + "; " + qFontToCss(font)); + const QString font = m_profile->getSettings()->value("Coloring/Fonts/" + key).toString(); + + QString fontCss; + static QMap fontCssCache; + if (fontCssCache.contains(font)) { + fontCss = fontCssCache[font]; + } else { + QFont qFont; + qFont.fromString(m_profile->getSettings()->value("Coloring/Fonts/" + key).toString()); + fontCss = qFontToCss(qFont); + } + + styles.insert(key, "color:" + color + "; " + fontCss); } QStringList t; t.reserve(tags.count()); - for (const Tag &tag : tags) + for (const Tag &tag : tags) { t.append(stylished(tag, styles, count, noUnderscores)); + } return t; } @@ -47,21 +59,28 @@ QString TagStylist::stylished(const Tag &tag, const QMap &styl // Guess the correct tag family const QString plural = tag.type().name() + "s"; QString key = styles.contains(plural) ? plural : "generals"; - if (m_profile->getBlacklist().contains(tag.text())) + if (m_profile->getBlacklist().contains(tag.text())) { key = "blacklisteds"; - if (m_profile->getIgnored().contains(tag.text(), Qt::CaseInsensitive)) + } + if (m_profile->getIgnored().contains(tag.text(), Qt::CaseInsensitive)) { key = "ignoreds"; - for (const QString &t : qAsConst(m_profile->getKeptForLater())) - if (t == tag.text()) + } + for (const QString &t : qAsConst(m_profile->getKeptForLater())) { + if (t == tag.text()) { key = "keptForLater"; - for (const Favorite &fav : qAsConst(m_profile->getFavorites())) - if (fav.getName() == tag.text()) + } + } + for (const Favorite &fav : qAsConst(m_profile->getFavorites())) { + if (fav.getName() == tag.text()) { key = "favorites"; + } + } QString txt = tag.text(); - QString ret = QString(R"(%3)").arg(tag.text(), styles.value(key), noUnderscores ? txt.replace('_', ' ') : tag.text()); - if (count && tag.count() > 0) + QString ret = QString(R"(%3)").arg(QUrl::toPercentEncoding(tag.text()), styles.value(key), noUnderscores ? txt.replace('_', ' ') : tag.text()); + if (count && tag.count() > 0) { ret += QString(" (%L1)").arg(tag.count()); + } return ret; } diff --git a/lib/src/tags/tag-type.cpp b/lib/src/tags/tag-type.cpp index 85d7175ee..41f439b4e 100644 --- a/lib/src/tags/tag-type.cpp +++ b/lib/src/tags/tag-type.cpp @@ -9,11 +9,11 @@ TagType::TagType(const QString &name) : m_isUnknown(name.isEmpty() || name == "unknown"), m_name(name.isEmpty() ? "unknown" : name) { // Sometimes a type is found with multiple words, only the first is relevant - if (!m_isUnknown) - { + if (!m_isUnknown) { const int typeSpace = m_name.indexOf(' '); - if (typeSpace != -1) - { m_name = m_name.left(typeSpace); } + if (typeSpace != -1) { + m_name = m_name.left(typeSpace); + } } } diff --git a/lib/src/tags/tag.cpp b/lib/src/tags/tag.cpp index b1c252397..cf47a0383 100644 --- a/lib/src/tags/tag.cpp +++ b/lib/src/tags/tag.cpp @@ -1,19 +1,20 @@ #include "tag.h" +#include #include "functions.h" #include "tag-type.h" Tag::Tag() : m_id(0), m_type(TagType()), m_count(0) -{ } +{} Tag::Tag(const QString &text, const QString &type, int count, const QStringList &related) : Tag(text, TagType(type), count, related) -{ } +{} Tag::Tag(const QString &text, const TagType &type, int count, const QStringList &related) : Tag(0, text, type, count, related) -{ } +{} Tag::Tag(int id, const QString &text, TagType type, int count, QStringList related) : m_id(id), m_type(std::move(type)), m_count(count), m_related(std::move(related)) @@ -23,18 +24,15 @@ Tag::Tag(int id, const QString &text, TagType type, int count, QStringList relat // Decode HTML entities in the tag text m_text = decodeHtmlEntities(text).replace(' ', '_'); - if (m_type.isUnknown() || weakTypes.contains(m_type.name())) - { + if (m_type.isUnknown() || weakTypes.contains(m_type.name())) { // Some artist names end with " (artist)" so we can guess their type - if (m_text.endsWith(QLatin1String("(artist)"))) - { + if (m_text.endsWith(QLatin1String("(artist)"))) { m_type = TagType(QStringLiteral("artist")); m_text = m_text.left(m_text.length() - 9); } const int sepPos = m_text.indexOf(':'); - if (sepPos != -1) - { + if (sepPos != -1) { static QMap prep = { { 0, QStringLiteral("artist") }, @@ -49,8 +47,7 @@ Tag::Tag(int id, const QString &text, TagType type, int count, QStringList relat const QString pre = Tag::GetType(m_text.left(sepPos)); const int prepIndex = prep.key(pre, -1); - if (prepIndex != -1) - { + if (prepIndex != -1) { m_type = TagType(Tag::GetType(prep[prepIndex], prep)); m_text = m_text.mid(sepPos + 1); } @@ -61,34 +58,91 @@ Tag::Tag(int id, const QString &text, TagType type, int count, QStringList relat QString Tag::GetType(QString type, QMap ids) { type = type.toLower().trimmed(); - if (type.contains(", ")) + if (type.contains(", ")) { type = type.split(", ").at(0).trimmed(); + } - if (type == QLatin1String("series")) + if (type == QLatin1String("idol")) { + return QStringLiteral("model"); + } + if (type == QLatin1String("series")) { return QStringLiteral("copyright"); - if (type == QLatin1String("mangaka")) + } + if (type == QLatin1String("mangaka")) { return QStringLiteral("artist"); - if (type == QLatin1String("game")) + } + if (type == QLatin1String("game")) { return QStringLiteral("copyright"); - if (type == QLatin1String("studio")) + } + if (type == QLatin1String("studio")) { return QStringLiteral("circle"); - if (type == QLatin1String("source")) + } + if (type == QLatin1String("source")) { return QStringLiteral("general"); - if (type == QLatin1String("character group")) + } + if (type == QLatin1String("character group")) { return QStringLiteral("general"); - if (type == QLatin1String("oc")) + } + if (type == QLatin1String("oc")) { return QStringLiteral("character"); + } - if (type.length() == 1) - { + if (type.length() == 1) { const int typeId = type.toInt(); - if (ids.contains(typeId)) + if (ids.contains(typeId)) { return ids[typeId]; + } } return type; } + +void Tag::write(QJsonObject &json) const +{ + json["text"] = m_text; + + if (m_id > 0) { + json["id"] = m_id; + } + if (!m_type.isUnknown()) { + json["type"] = m_type.name(); + } + if (m_count >= 0) { + json["count"] = m_count; + } + if (!m_related.isEmpty()) { + json["related"] = QJsonArray::fromStringList(m_related); + } +} + +bool Tag::read(const QJsonObject &json) +{ + m_text = json["text"].toString(); + + if (json.contains("id")) { + m_id = json["id"].toInt(); + } + if (json.contains("type")) { + m_type = TagType(json["type"].toString()); + } + if (json.contains("count")) { + m_count = json["count"].toInt(); + } + + // Related + if (json.contains("related")) { + QJsonArray related = json["related"].toArray(); + m_related.reserve(related.count()); + for (auto tag : related) { + m_related.append(tag.toString()); + } + } + + return true; +} + + void Tag::setId(int id) { m_id = id; } void Tag::setText(const QString &text) { m_text = text; } void Tag::setType(const TagType &type) { m_type = type; } diff --git a/lib/src/tags/tag.h b/lib/src/tags/tag.h index e79ec05f5..5f6dca5d1 100644 --- a/lib/src/tags/tag.h +++ b/lib/src/tags/tag.h @@ -1,6 +1,7 @@ #ifndef TAG_H #define TAG_H +#include #include #include #include @@ -16,11 +17,19 @@ class Tag explicit Tag(const QString &text, const TagType &type, int count = 0, const QStringList &related = QStringList()); explicit Tag(int id, const QString &text, TagType type, int count = 0, QStringList related = QStringList()); static QString GetType(QString type, QMap ids = QMap()); + + // Serialization + void write(QJsonObject &json) const; + bool read(const QJsonObject &json); + + // Setters void setId(int id); void setText(const QString &text); void setType(const TagType &type); void setCount(int count); void setRelated(const QStringList &related); + + // Getters int id() const; const QString &text() const; const TagType &type() const; diff --git a/lib/src/updater/program-updater.cpp b/lib/src/updater/program-updater.cpp index aeb538a7e..bbe80821e 100644 --- a/lib/src/updater/program-updater.cpp +++ b/lib/src/updater/program-updater.cpp @@ -12,11 +12,11 @@ ProgramUpdater::ProgramUpdater() : ProgramUpdater(QStringLiteral("https://api.github.com/repos/Bionus/imgbrd-grabber")) -{ } +{} ProgramUpdater::ProgramUpdater(QString baseUrl) : m_baseUrl(std::move(baseUrl)), m_downloadReply(nullptr) -{ } +{} void ProgramUpdater::checkForUpdates() const { @@ -83,8 +83,7 @@ void ProgramUpdater::downloadUpdate() void ProgramUpdater::downloadDone() { QUrl redirection = m_downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); - if (!redirection.isEmpty()) - { + if (!redirection.isEmpty()) { log(QStringLiteral("Installer download redirected to \"%1\".").arg(redirection.toString())); const QNetworkRequest request(redirection); m_downloadReply = m_networkAccessManager->get(request); @@ -94,8 +93,7 @@ void ProgramUpdater::downloadDone() } QFile file(QDir::tempPath() + QDir::separator() + m_updateFilename); - if (!file.open(QFile::WriteOnly | QFile::Truncate)) - { + if (!file.open(QFile::WriteOnly | QFile::Truncate)) { log(QStringLiteral("Error opening installer file \"%1\".").arg(file.fileName())); return; } diff --git a/lib/src/updater/source-updater.cpp b/lib/src/updater/source-updater.cpp index 39b4df764..844e800fa 100644 --- a/lib/src/updater/source-updater.cpp +++ b/lib/src/updater/source-updater.cpp @@ -8,8 +8,9 @@ SourceUpdater::SourceUpdater(QString source, QString directory, QString baseUrl) : m_source(std::move(source)), m_directory(std::move(directory)), m_baseUrl(std::move(baseUrl)) { - if (!m_baseUrl.endsWith("/")) + if (!m_baseUrl.endsWith("/")) { m_baseUrl += "/"; + } } @@ -27,22 +28,22 @@ void SourceUpdater::checkForUpdatesDone() auto *reply = dynamic_cast(sender()); bool isNew = false; - QString source = reply->readAll(); - if (source.startsWith("readAll(); + if (source.startsWith("deleteLater(); diff --git a/lib/src/updater/updater.cpp b/lib/src/updater/updater.cpp index 15205bc02..6ab52c2ca 100644 --- a/lib/src/updater/updater.cpp +++ b/lib/src/updater/updater.cpp @@ -17,8 +17,7 @@ int Updater::compareVersions(QString a, QString b) int aSub = 0; char aSubType = ' '; const int aPos = a.indexOf(QRegularExpression("[a-z]")); - if (aPos != -1) - { + if (aPos != -1) { aSubType = a[aPos].toLatin1(); aSub = a.midRef(aPos + 1).toInt(); a = a.left(aPos); @@ -27,8 +26,7 @@ int Updater::compareVersions(QString a, QString b) int bSub = 0; char bSubType = ' '; const int bPos = b.indexOf(QRegularExpression("[a-z]")); - if (bPos != -1) - { + if (bPos != -1) { bSubType = b[bPos].toLatin1(); bSub = b.midRef(bPos + 1).toInt(); b = b.left(bPos); @@ -37,36 +35,39 @@ int Updater::compareVersions(QString a, QString b) QStringList aSem = a.split('.'); QStringList bSem = b.split('.'); - if (aSem.count() != bSem.count()) - return 0; + for (int i = 0; i < qMax(aSem.count(), bSem.count()); ++i) { + const int aPart = i < aSem.count() ? aSem[i].toInt() : 0; + const int bPart = i < bSem.count() ? bSem[i].toInt() : 0; - for (int i = 0; i < aSem.count(); ++i) - { - const int aPart = aSem[i].toInt(); - const int bPart = bSem[i].toInt(); - - if (aPart > bPart) + if (aPart > bPart) { return 1; - if (aPart < bPart) + } + if (aPart < bPart) { return -1; + } } - if (aSubType == ' ' && bSubType != ' ') + if (aSubType == ' ' && bSubType != ' ') { return 1; - if (aSubType != ' ' && bSubType == ' ') + } + if (aSubType != ' ' && bSubType == ' ') { return -1; + } - if (aSubType != ' ' && bSubType != ' ') - { - if (aSubType > bSubType) + if (aSubType != ' ' && bSubType != ' ') { + if (aSubType > bSubType) { return 1; - if (aSubType < bSubType) + } + if (aSubType < bSubType) { return -1; + } - if (aSub > bSub) + if (aSub > bSub) { return 1; - if (aSub < bSub) + } + if (aSub < bSub) { return -1; + } } return 0; diff --git a/lib/src/updater/updater.h b/lib/src/updater/updater.h index 4bbf2a619..ff3cdd8b5 100644 --- a/lib/src/updater/updater.h +++ b/lib/src/updater/updater.h @@ -11,8 +11,10 @@ class Updater : public QObject { Q_OBJECT - public: + protected: Updater(); + + public: ~Updater() override; int compareVersions(QString a, QString b); diff --git a/lib/src/vendor/ganalytics.cpp b/lib/src/vendor/ganalytics.cpp new file mode 100644 index 000000000..9f4c78ea6 --- /dev/null +++ b/lib/src/vendor/ganalytics.cpp @@ -0,0 +1,944 @@ +#include "ganalytics.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef QT_GUI_LIB +#include +#include +#endif // QT_GUI_LIB + +#ifdef QT_QML_LIB +#include +#include +#endif // QT_QML_LIB + +#include "functions.h" + +struct QueryBuffer +{ + QUrlQuery postQuery; + QDateTime time; +}; + +/** + * Class Private + * Private members and functions. + */ +class GAnalytics::Private : public QObject +{ + Q_OBJECT + +public: + explicit Private(GAnalytics *parent = 0); + ~Private(); + + GAnalytics *q; + + QNetworkAccessManager *networkManager; + + QQueue messageQueue; + QTimer timer; + QNetworkRequest request; + GAnalytics::LogLevel logLevel; + + QString trackingID; + QString clientID; + QString userID; + QString appName; + QString appVersion; + QString language; + QString screenResolution; + QString viewportSize; + + bool isSending; + + const static int fourHours = 4 * 60 * 60 * 1000; + const static QString dateTimeFormat; + +public: + void logMessage(GAnalytics::LogLevel level, const QString &message); + + QUrlQuery buildStandardPostQuery(const QString &type); +#ifdef QT_GUI_LIB + QString getScreenResolution(); +#endif // QT_GUI_LIB + QString getUserAgent(); + QString getSystemInfo(); + QList persistMessageQueue(); + void readMessagesFromFile(const QList &dataList); + QString getClientID(); + QString getUserID(); + void setUserID(const QString &userID); + void enqueQueryWithCurrentTime(const QUrlQuery &query); + void setIsSending(bool doSend); + +signals: + void postNextMessage(); + +public slots: + void postMessage(); + void postMessageFinished(); +}; + +const QString GAnalytics::Private::dateTimeFormat = "yyyy,MM,dd-hh:mm::ss:zzz"; + +/** + * Constructor + * Constructs an object of class Private. + * @param parent + */ +GAnalytics::Private::Private(GAnalytics *parent) +: QObject(parent) +, q(parent) +, networkManager(NULL) +, request(QUrl("http://www.google-analytics.com/collect")) +, logLevel(GAnalytics::Error) +, isSending(false) +{ + clientID = getClientID(); + userID = getUserID(); + language = QLocale::system().name().toLower().replace("_", "-"); +#ifdef QT_GUI_LIB + screenResolution = getScreenResolution(); +#endif // QT_GUI_LIB + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + appName = QCoreApplication::instance()->applicationName(); + appVersion = QCoreApplication::instance()->applicationVersion(); + request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent()); + connect(this, SIGNAL(postNextMessage()), this, SLOT(postMessage())); + timer.start(30000); + connect(&timer, SIGNAL(timeout()), this, SLOT(postMessage())); +} + +/** + * Destructor + * Delete an object of class Private. + */ +GAnalytics::Private::~Private() +{ +} + +void GAnalytics::Private::logMessage(LogLevel level, const QString &message) +{ + if (logLevel > level) + { + return; + } + + qDebug() << "[Analytics]" << message; +} + +/** + * Build the POST query. Adds all parameter to the query + * which are used in every POST. + * @param type Type of POST message. The event which is to post. + * @return query Most used parameter in a query for a POST. + */ +QUrlQuery GAnalytics::Private::buildStandardPostQuery(const QString &type) +{ + QUrlQuery query; + query.addQueryItem("v", "1"); + query.addQueryItem("tid", trackingID); + query.addQueryItem("cid", clientID); + if(!userID.isEmpty()) + { + query.addQueryItem("uid", userID); + } + query.addQueryItem("t", type); + query.addQueryItem("ul", language); + +#ifdef QT_GUI_LIB + query.addQueryItem("vp", viewportSize); + query.addQueryItem("sr", screenResolution); +#endif // QT_GUI_LIB + + return query; +} + +#ifdef QT_GUI_LIB +/** + * Get devicese screen resolution. + * @return A QString like "800x600". + */ +QString GAnalytics::Private::getScreenResolution() +{ + QScreen *screen = QGuiApplication::primaryScreen(); + QSize size = screen->size(); + + return QString("%1x%2").arg(size.width()).arg(size.height()); +} +#endif // QT_GUI_LIB + + +/** + * Try to gain information about the system where this application + * is running. It needs to get the name and version of the operating + * system, the language and screen resolution. + * All this information will be send in POST messages. + * @return agent A QString with all the information formatted for a POST message. + */ +QString GAnalytics::Private::getUserAgent() +{ + QString locale = QLocale::system().name(); + QString system = getSystemInfo(); + + return QString("%1/%2 (%3; %4) GAnalytics/1.0 (Qt/%5)").arg(appName).arg(appVersion).arg(system).arg(locale).arg(QT_VERSION_STR); +} + + +#ifdef Q_OS_MAC +/** + * Only on Mac OS X + * Get the Operating system name and version. + * @return os The operating system name and version in a string. + */ +QString GAnalytics::Private::getSystemInfo() +{ + QSysInfo::MacVersion version = QSysInfo::macVersion(); + QString os; + switch (version) + { + case QSysInfo::MV_9: + os = "Macintosh; Mac OS 9"; + break; + case QSysInfo::MV_10_0: + os = "Macintosh; Mac OS 10.0"; + break; + case QSysInfo::MV_10_1: + os = "Macintosh; Mac OS 10.1"; + break; + case QSysInfo::MV_10_2: + os = "Macintosh; Mac OS 10.2"; + break; + case QSysInfo::MV_10_3: + os = "Macintosh; Mac OS 10.3"; + break; + case QSysInfo::MV_10_4: + os = "Macintosh; Mac OS 10.4"; + break; + case QSysInfo::MV_10_5: + os = "Macintosh; Mac OS 10.5"; + break; + case QSysInfo::MV_10_6: + os = "Macintosh; Mac OS 10.6"; + break; + case QSysInfo::MV_10_7: + os = "Macintosh; Mac OS 10.7"; + break; + case QSysInfo::MV_10_8: + os = "Macintosh; Mac OS 10.8"; + break; + case QSysInfo::MV_10_9: + os = "Macintosh; Mac OS 10.9"; + break; + case QSysInfo::MV_10_10: + os = "Macintosh; Mac OS 10.10"; + break; + case QSysInfo::MV_10_11: + os = "Macintosh; Mac OS 10.11"; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case QSysInfo::MV_10_12: + os = "Macintosh; Mac OS 10.12"; + break; +#endif + case QSysInfo::MV_Unknown: + os = "Macintosh; Mac OS unknown"; + break; + case QSysInfo::MV_IOS_5_0: + os = "iPhone; iOS 5.0"; + break; + case QSysInfo::MV_IOS_5_1: + os = "iPhone; iOS 5.1"; + break; + case QSysInfo::MV_IOS_6_0: + os = "iPhone; iOS 6.0"; + break; + case QSysInfo::MV_IOS_6_1: + os = "iPhone; iOS 6.1"; + break; + case QSysInfo::MV_IOS_7_0: + os = "iPhone; iOS 7.0"; + break; + case QSysInfo::MV_IOS_7_1: + os = "iPhone; iOS 7.1"; + break; + case QSysInfo::MV_IOS_8_0: + os = "iPhone; iOS 8.0"; + break; + case QSysInfo::MV_IOS_8_1: + os = "iPhone; iOS 8.1"; + break; + case QSysInfo::MV_IOS_8_2: + os = "iPhone; iOS 8.2"; + break; + case QSysInfo::MV_IOS_8_3: + os = "iPhone; iOS 8.3"; + break; + case QSysInfo::MV_IOS_8_4: + os = "iPhone; iOS 8.4"; + break; + case QSysInfo::MV_IOS_9_0: + os = "iPhone; iOS 9.0"; + break; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)) + case QSysInfo::MV_IOS_9_1: + os = "iPhone; iOS 9.1"; + break; + case QSysInfo::MV_IOS_9_2: + os = "iPhone; iOS 9.2"; + break; + case QSysInfo::MV_IOS_9_3: + os = "iPhone; iOS 9.3"; + break; + case QSysInfo::MV_IOS_10_0: + os = "iPhone; iOS 10.0"; + break; +#endif + case QSysInfo::MV_IOS: + os = "iPhone; iOS unknown"; + break; + default: + os = "Macintosh"; + break; + } + return os; +} +#endif + +#ifdef Q_OS_WIN +/** + * Only on Windows + * Get operating system and its version. + * @return os A QString containing the oprating systems name and version. + */ +QString GAnalytics::Private::getSystemInfo() +{ + QSysInfo::WinVersion version = QSysInfo::windowsVersion(); + QString os("Windows; "); + switch (version) + { + case QSysInfo::WV_95: + os += "Win 95"; + break; + case QSysInfo::WV_98: + os += "Win 98"; + break; + case QSysInfo::WV_Me: + os += "Win ME"; + break; + case QSysInfo::WV_NT: + os += "Win NT"; + break; + case QSysInfo::WV_2000: + os += "Win 2000"; + break; + case QSysInfo::WV_2003: + os += "Win Server 2003"; + break; + case QSysInfo::WV_VISTA: + os += "Win Vista"; + break; + case QSysInfo::WV_WINDOWS7: + os += "Win 7"; + break; + case QSysInfo::WV_WINDOWS8: + os += "Win 8"; + break; + case QSysInfo::WV_WINDOWS8_1: + os += "Win 8.1"; + break; + case QSysInfo::WV_WINDOWS10: + os += "Win 10"; + break; + default: + os = "Windows; unknown"; + break; + } + return os; +} +#endif + +#if defined(Q_OS_ANDROID) +#include + +QString GAnalytics::Private::getSystemInfo() +{ + return QString("Linux; U; Android %1; %2 %3 Build/%4; %5") + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build$VERSION", "RELEASE").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "MANUFACTURER").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "MODEL").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "ID").toString()) + .arg(QAndroidJniObject::getStaticObjectField("android/os/Build", "BRAND").toString()); +} +#elif defined(Q_OS_LINUX) +#include + +/** + * Only on Unix systems. + * Get operation system name and version. + * @return os A QString with the name and version of the operating system. + */ +QString GAnalytics::Private::getSystemInfo() +{ + struct utsname buf; + uname(&buf); + QString system(buf.sysname); + QString release(buf.release); + + return system + "; " + release; +} +#endif + + +/** + * The message queue contains a list of QueryBuffer object. + * QueryBuffer holds a QUrlQuery object and a QDateTime object. + * These both object are freed from the buffer object and + * inserted as QString objects in a QList. + * @return dataList The list with concartinated queue data. + */ +QList GAnalytics::Private::persistMessageQueue() +{ + QList dataList; + foreach (QueryBuffer buffer, messageQueue) + { + dataList << buffer.postQuery.toString(); + dataList << buffer.time.toString(dateTimeFormat); + } + + return dataList; +} + +/** + * Reads persistent messages from a file. + * Gets all message data as a QList. + * Two lines in the list build a QueryBuffer object. + */ +void GAnalytics::Private::readMessagesFromFile(const QList &dataList) +{ + QListIterator iter(dataList); + while (iter.hasNext()) + { + QString queryString = iter.next(); + if(!iter.hasNext()) + break; + QString dateString = iter.next(); + if(queryString.isEmpty() || dateString.isEmpty()) + break; + QUrlQuery query; + query.setQuery(queryString); + QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat); + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = dateTime; + messageQueue.enqueue(buffer); + } +} + +/** + * Change the user id. + * @param userID A string with the user id. + */ +void GAnalytics::Private::setUserID(const QString &userID) +{ + this->userID = userID; + QSettings settings(savePath("settings.ini"), QSettings::IniFormat); + settings.setValue("GAnalytics/uid", userID); +} + +/** + * Get the user id. + * User id once created is stored in application settings. + * @return userID A string with the user id. + */ +QString GAnalytics::Private::getUserID() +{ + QSettings settings(savePath("settings.ini"), QSettings::IniFormat); + QString userID = settings.value("GAnalytics/uid", QString("")).toString(); + + return userID; +} + +/** + * Get the client id. + * Client id once created is stored in application settings. + * @return clientID A string with the client id. + */ +QString GAnalytics::Private::getClientID() +{ + QSettings settings(savePath("settings.ini"), QSettings::IniFormat); + QString clientID; + if (!settings.contains("GAnalytics/cid")) + { + clientID = QUuid::createUuid().toString().mid(1, 36); + settings.setValue("GAnalytics/cid", clientID); + } + else + { + clientID = settings.value("GAnalytics/cid").toString(); + } + + return clientID; +} + +/** + * Takes a QUrlQuery object and wrapp it together with + * a QTime object into a QueryBuffer struct. These struct + * will be stored in the message queue. + * @param query + */ +void GAnalytics::Private::enqueQueryWithCurrentTime(const QUrlQuery &query) +{ + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = QDateTime::currentDateTime(); + + messageQueue.enqueue(buffer); +} + +/** + * Change status of class. Emit signal that status was changed. + * @param doSend + */ +void GAnalytics::Private::setIsSending(bool doSend) +{ + if (doSend) + { + timer.stop(); + } + else + { + timer.start(); + } + + bool changed = (isSending != doSend); + + isSending = doSend; + + if (changed) + { + emit q->isSendingChanged(isSending); + } +} + + +/** + * CONSTRUCTOR GAnalytics + * ------------------------------------------------------------------------------------------------------------ + * Constructs the GAnalytics Object. + * @param parent The application which uses this object. + * @param trackingID + * @param clientID + * @param withGet Determines wheather the messages are send with GET or POST. + */ +GAnalytics::GAnalytics(QObject *parent) +: QObject(parent) +, d(new Private(this)) +{ +} + +GAnalytics::GAnalytics(const QString &trackingID, QObject *parent) +: QObject(parent) +, d(new Private(this)) +{ + setTrackingID(trackingID); +} + +/** + * Destructor of class GAnalytics. + */ +GAnalytics::~GAnalytics() +{ + delete d; +} + +void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel) +{ + if (d->logLevel != logLevel) + { + d->logLevel = logLevel; + emit logLevelChanged(); + } +} + +GAnalytics::LogLevel GAnalytics::logLevel() const +{ + return d->logLevel; +} + +// SETTER and GETTER +void GAnalytics::setViewportSize(const QString &viewportSize) +{ + if (d->viewportSize != viewportSize) + { + d->viewportSize = viewportSize; + emit viewportSizeChanged(); + } +} + +QString GAnalytics::viewportSize() const +{ + return d->viewportSize; +} + +void GAnalytics::setLanguage(const QString &language) +{ + if (d->language != language) + { + d->language = language; + emit languageChanged(); + } +} + +QString GAnalytics::language() const +{ + return d->language; +} + +void GAnalytics::setTrackingID(const QString &trackingID) +{ + if (d->trackingID != trackingID) + { + d->trackingID = trackingID; + emit trackingIDChanged(); + } +} + +QString GAnalytics::trackingID() const +{ + return d->trackingID; +} + +void GAnalytics::setSendInterval(int milliseconds) +{ + if (d->timer.interval() != milliseconds) + { + d->timer.setInterval(milliseconds); + emit sendIntervalChanged(); + } +} + +void GAnalytics::setUserID(const QString &userID) +{ + if(d->userID != userID) + { + d->setUserID(userID); + emit userIDChanged(); + } +} + +QString GAnalytics::userID() const +{ + return d->getUserID(); +} + +int GAnalytics::sendInterval() const +{ + return (d->timer.interval()); +} + +void GAnalytics::startSending() +{ + if (!isSending()) + emit d->postNextMessage(); +} + +bool GAnalytics::isSending() const +{ + return d->isSending; +} + +void GAnalytics::setNetworkAccessManager(QNetworkAccessManager *networkAccessManager) +{ + if (d->networkManager != networkAccessManager) + { + // Delete the old network manager if it was our child + if (d->networkManager && d->networkManager->parent() == this) + { + d->networkManager->deleteLater(); + } + + d->networkManager = networkAccessManager; + } +} + +QNetworkAccessManager *GAnalytics::networkAccessManager() const +{ + return d->networkManager; +} + +static void appendCustomValues(QUrlQuery &query, const QVariantMap &customValues) { + for(QVariantMap::const_iterator iter = customValues.begin(); iter != customValues.end(); ++iter) { + query.addQueryItem(iter.key(), iter.value().toString()); + } +} + + +/** +* SentAppview is called when the user changed the applications view. +* Deprecated because after SDK Version 3.08 and up no more "appview" event: +* Use sendScreenView() instead +* @param appName +* @param appVersion +* @param screenName +*/ +void GAnalytics::sendAppView(const QString &screenName, + const QVariantMap &customValues) +{ + sendScreenView(screenName, customValues); +} + +/** + * Sent screen view is called when the user changed the applications view. + * These action of the user should be noticed and reported. Therefore + * a QUrlQuery is build in this method. It holts all the parameter for + * a http POST. The UrlQuery will be stored in a message Queue. + * @param appName + * @param appVersion + * @param screenName + */ +void GAnalytics::sendScreenView(const QString &screenName, + const QVariantMap &customValues) +{ + d->logMessage(Info, QString("ScreenView: %1").arg(screenName)); + + QUrlQuery query = d->buildStandardPostQuery("screenview"); + query.addQueryItem("cd", screenName); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * This method is called whenever a button was pressed in the application. + * A query for a POST message will be created to report this event. The + * created query will be stored in a message queue. + * @param eventCategory + * @param eventAction + * @param eventLabel + * @param eventValue + */ +void GAnalytics::sendEvent(const QString &category, const QString &action, + const QString &label, const QVariant &value, + const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("event"); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + query.addQueryItem("ec", category); + query.addQueryItem("ea", action); + if (! label.isEmpty()) + query.addQueryItem("el", label); + if (value.isValid()) + query.addQueryItem("ev", value.toString()); + + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Method is called after an exception was raised. It builds a + * query for a POST message. These query will be stored in a + * message queue. + * @param exceptionDescription + * @param exceptionFatal + */ +void GAnalytics::sendException(const QString &exceptionDescription, + bool exceptionFatal, + const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("exception"); + query.addQueryItem("an", d->appName); + query.addQueryItem("av", d->appVersion); + + query.addQueryItem("exd", exceptionDescription); + + if (exceptionFatal) + { + query.addQueryItem("exf", "1"); + } + else + { + query.addQueryItem("exf", "0"); + } + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Session starts. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::startSession() +{ + QVariantMap customValues; + customValues.insert("sc", "start"); + sendEvent("Session", "Start", QString(), QVariant(), customValues); +} + +/** + * Session ends. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::endSession() +{ + QVariantMap customValues; + customValues.insert("sc", "end"); + sendEvent("Session", "End", QString(), QVariant(), customValues); +} + +/** + * This function is called by a timer interval. + * The function tries to send a messages from the queue. + * If message was successfully send then this function + * will be called back to send next message. + * If message queue contains more than one message then + * the connection will kept open. + * The message POST is asyncroniously when the server + * answered a signal will be emitted. + */ +void GAnalytics::Private::postMessage() +{ + if (messageQueue.isEmpty()) + { + setIsSending(false); + return; + } + else + { + setIsSending(true); + } + + QString connection = "close"; + if (messageQueue.count() > 1) + { + connection = "keep-alive"; + } + + QueryBuffer buffer = messageQueue.head(); + QDateTime sendTime = QDateTime::currentDateTime(); + qint64 timeDiff = buffer.time.msecsTo(sendTime); + + if(timeDiff > fourHours) + { + // too old. + messageQueue.dequeue(); + emit postNextMessage(); + return; + } + + buffer.postQuery.addQueryItem("qt", QString::number(timeDiff)); + request.setRawHeader("Connection", connection.toUtf8()); + QByteArray ba; + ba = buffer.postQuery.query(QUrl::FullyEncoded).toUtf8(); + request.setHeader(QNetworkRequest::ContentLengthHeader, ba.length()); + + // Create a new network access manager if we don't have one yet + if (networkManager == NULL) + { + networkManager = new QNetworkAccessManager(this); + } + + QNetworkReply *reply = networkManager->post(request, ba); + connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished())); +} + +/** + * NetworkAccsessManager has finished to POST a message. + * If POST message was successfully send then the message + * query should be removed from queue. + * SIGNAL "postMessage" will be emitted to send next message + * if there is any. + * If message couldn't be send then next try is when the + * timer emits its signal. + */ +void GAnalytics::Private::postMessageFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + reply->deleteLater(); + + int httpStausCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (httpStausCode < 200 || httpStausCode > 299) + { + logMessage(GAnalytics::Error, QString("Error posting message: %s").arg(reply->errorString())); + + // An error ocurred. + setIsSending(false); + return; + } + else + { + logMessage(GAnalytics::Debug, "Message sent"); + } + + messageQueue.dequeue(); + emit postNextMessage(); +} + + +/** + * Qut stream to persist class GAnalytics. + * @param outStream + * @param analytics + * @return + */ +QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics) +{ + outStream << analytics.d->persistMessageQueue(); + + return outStream; +} + + +/** + * In stream to read GAnalytics from file. + * @param inStream + * @param analytics + * @return + */ +QDataStream &operator >>(QDataStream &inStream, GAnalytics &analytics) +{ + QList dataList; + inStream >> dataList; + analytics.d->readMessagesFromFile(dataList); + + return inStream; +} + +#ifdef QT_QML_LIB +void GAnalytics::classBegin() +{ + // Get the network access manager from the QmlEngine + QQmlContext *context = QQmlEngine::contextForObject(this); + if (context) + { + QQmlEngine *engine = context->engine(); + setNetworkAccessManager(engine->networkAccessManager()); + } +} + +void GAnalytics::componentComplete() +{ +} +#endif // QT_QML_LIB + +#include "ganalytics.moc" diff --git a/lib/src/vendor/ganalytics.h b/lib/src/vendor/ganalytics.h new file mode 100644 index 000000000..93e413f39 --- /dev/null +++ b/lib/src/vendor/ganalytics.h @@ -0,0 +1,114 @@ +#ifndef GANALYTICS_H +#define GANALYTICS_H + +#include +#include + +#ifdef QT_QML_LIB +#include +#endif // QT_QML_LIB + +class QNetworkAccessManager; + +class GAnalytics : public QObject +#ifdef QT_QML_LIB + , public QQmlParserStatus +#endif // QT_QML_LIB +{ + Q_OBJECT +#ifdef QT_QML_LIB + Q_INTERFACES(QQmlParserStatus) +#endif // QT_QML_LIB + Q_ENUMS(LogLevel) + Q_PROPERTY(LogLevel logLevel READ logLevel WRITE setLogLevel NOTIFY logLevelChanged) + Q_PROPERTY(QString viewportSize READ viewportSize WRITE setViewportSize NOTIFY viewportSizeChanged) + Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) + Q_PROPERTY(QString trackingID READ trackingID WRITE setTrackingID NOTIFY trackingIDChanged) + Q_PROPERTY(QString userID READ userID WRITE setUserID NOTIFY userIDChanged) + Q_PROPERTY(int sendInterval READ sendInterval WRITE setSendInterval NOTIFY sendIntervalChanged) + Q_PROPERTY(bool isSending READ isSending NOTIFY isSendingChanged) + +public: + explicit GAnalytics(QObject *parent = 0); + explicit GAnalytics(const QString &trackingID, QObject *parent = 0); + ~GAnalytics(); + +public: + enum LogLevel + { + Debug, + Info, + Error, + None + }; + + void setLogLevel(LogLevel logLevel); + LogLevel logLevel() const; + + // Getter and Setters + void setViewportSize(const QString &viewportSize); + QString viewportSize() const; + + void setLanguage(const QString &language); + QString language() const; + + void setTrackingID(const QString &trackingID); + QString trackingID() const; + + void setUserID(const QString &userID); + QString userID() const; + + void setSendInterval(int milliseconds); + int sendInterval() const; + + void startSending(); + bool isSending() const; + + /// Get or set the network access manager. If none is set, the class creates its own on the first request + void setNetworkAccessManager(QNetworkAccessManager *networkAccessManager); + QNetworkAccessManager *networkAccessManager() const; + +#ifdef QT_QML_LIB + // QQmlParserStatus interface + void classBegin(); + void componentComplete(); +#endif // QT_QML_LIB + +public slots: + void sendScreenView(const QString &screenName, + const QVariantMap &customValues = QVariantMap()); + void sendAppView(const QString &screenName, + const QVariantMap &customValues = QVariantMap()); + void sendEvent(const QString &category, + const QString &action, + const QString &label = QString(), + const QVariant &value = QVariant(), + const QVariantMap &customValues = QVariantMap()); + void sendException(const QString &exceptionDescription, + bool exceptionFatal = true, + const QVariantMap &customValues = QVariantMap()); + void startSession(); + void endSession(); + + +signals: + void logLevelChanged(); + void viewportSizeChanged(); + void languageChanged(); + void trackingIDChanged(); + void userIDChanged(); + void sendIntervalChanged(); + void isSendingChanged(bool isSending); + +private: + class Private; + Private *d; + + friend QDataStream& operator<<(QDataStream &outStream, const GAnalytics &analytics); + friend QDataStream& operator>>(QDataStream &inStream, GAnalytics &analytics); +}; + +QDataStream& operator<<(QDataStream &outStream, const GAnalytics &analytics); +QDataStream& operator>>(QDataStream &inStream, GAnalytics &analytics); + +#endif // GANALYTICS_H diff --git a/lib/src/vendor/qcustomnetworkreply.cpp b/lib/src/vendor/qcustomnetworkreply.cpp index 80f1375af..ad41f3671 100644 --- a/lib/src/vendor/qcustomnetworkreply.cpp +++ b/lib/src/vendor/qcustomnetworkreply.cpp @@ -35,9 +35,14 @@ void QCustomNetworkReply::setNetworkError( QNetworkReply::NetworkError errorCode setError(errorCode, errorString); } -void QCustomNetworkReply::setHeader( QNetworkRequest::KnownHeaders header, const QVariant &value ) +void QCustomNetworkReply::setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) { - QNetworkReply::setHeader( header, value ); + QNetworkReply::setHeader(header, value); +} + +void QCustomNetworkReply::setAttribute(QNetworkRequest::Attribute code, const QVariant &value) +{ + QNetworkReply::setAttribute(code, value); } void QCustomNetworkReply::setContentType( const QByteArray &contentType ) diff --git a/lib/src/vendor/qcustomnetworkreply.h b/lib/src/vendor/qcustomnetworkreply.h index 26227a08f..6ec2f2354 100644 --- a/lib/src/vendor/qcustomnetworkreply.h +++ b/lib/src/vendor/qcustomnetworkreply.h @@ -15,6 +15,7 @@ class QCustomNetworkReply : public QNetworkReply void setHttpStatusCode(int code, const QByteArray &statusText = QByteArray()); void setNetworkError(QNetworkReply::NetworkError errorCode, const QString &errorString = QString()); void setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); + void setAttribute(QNetworkRequest::Attribute code, const QVariant &value); void setContentType(const QByteArray &contentType); void setContent(const QString &content); diff --git a/release/sites/Anime pictures/model.ts b/release/sites/Anime pictures/model.ts index a8bd80a7e..3233e895c 100644 --- a/release/sites/Anime pictures/model.ts +++ b/release/sites/Anime pictures/model.ts @@ -1,19 +1,22 @@ +function noWebp(url: string): string { + return url.replace(/(\.\w{3,4})\.webp/, "$1"); +} + function completeImage(img: IImage): IImage { - if (img["ext"] && img["ext"][0] === ".") { - img["ext"] = img["ext"].substring(1); + if (img.ext && img.ext[0] === ".") { + img.ext = img.ext.substring(1); } - img["file_url"] = `/pictures/download_image/${img["id"]}.${img["ext"] || "jpg"}`; + img.file_url = `/pictures/download_image/${img.id}.${img.ext || "jpg"}`; - if ((!img["sample_url"] || img["sample_url"].length < 5) && img["preview_url"] && img["preview_url"].length >= 5) { - img["sample_url"] = img["preview_url"] + if ((!img.sample_url || img.sample_url.length < 5) && img.preview_url && img.preview_url.length >= 5) { + img.sample_url = img.preview_url .replace("_cp.", "_bp.") .replace("_sp.", "_bp."); } - img["file_url"] = img["file_url"].replace(".jpg.webp", ".jpg"); - img["sample_url"] = (img["sample_url"] || "").replace(".jpg.webp", ".jpg"); - img["preview_url"] = (img["preview_url"] || "").replace(".jpg.webp", ".jpg"); + img.sample_url = noWebp(img.sample_url || ""); + img.preview_url = noWebp(img.preview_url || ""); return img; } @@ -47,7 +50,7 @@ function searchToUrl(search: string): string { for (const tag of parts) { const part = tag.trim(); if (part.indexOf("width:") === 0) { - sizeToUrl(part.substr(6), "ret_x", ret); + sizeToUrl(part.substr(6), "res_x", ret); } else if (part.indexOf("height:") === 0) { sizeToUrl(part.substr(7), "res_y", ret); } else if (part.indexOf("ratio:") === 0) { @@ -68,27 +71,6 @@ function searchToUrl(search: string): string { return ret.join("&"); } -const auth: { [id: string]: IAuth } = { - session: { - type: "post", - url: "/login/submit", - fields: [ - { - key: "login", - type: "username", - }, - { - key: "password", - type: "password", - }, - ], - check: { - type: "cookie", - key: "asian_server", - }, - }, -}; - export const source: ISource = { name: "Anime pictures", modifiers: ["width:", "height:", "ratio:", "order:", "filetype:"], @@ -103,7 +85,27 @@ export const source: ISource = { parenthesis: false, precedence: "and", }, - auth, + auth: { + session: { + type: "post", + url: "/login/submit", + fields: [ + { + id: "pseudo", + key: "login", + }, + { + id: "password", + key: "password", + type: "password", + }, + ], + check: { + type: "cookie", + key: "asian_server", + }, + }, + }, apis: { json: { name: "JSON", @@ -137,7 +139,7 @@ export const source: ISource = { return { images, imageCount: data["posts_count"], - pageCount: data["max_pages"], + pageCount: data["max_pages"] + 1, // max_pages is an index, not a count, and pages start at 0 }; }, }, @@ -156,10 +158,14 @@ export const source: ISource = { }; }); + const imgUrl: string = data["file_url"]; + const pos = imgUrl.lastIndexOf("/") + 1; + const fn = imgUrl.substr(pos); + return { tags, createdAt: data["pubtime"], - imageUrl: data["file_url"], + imageUrl: imgUrl.substr(0, pos) + (fn.indexOf(" ") !== -1 ? encodeURIComponent(fn) : fn), }; }, }, diff --git a/release/sites/Anime pictures/model.xml b/release/sites/Anime pictures/model.xml deleted file mode 100644 index d3feb00e6..000000000 --- a/release/sites/Anime pictures/model.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - Anime pictures - - /pictures/download_image/{id}.jpg - _cp.->_bp.&_sp.->_bp. - - /pictures/view_posts/{page}?search_tag={tags}&lang=en&type=json - - - /pictures/view_posts/{page}?search_tag={tags}&lang=en - /pictures/view_post/{id}?lang=en - /pictures/view_all_tags/{page}?lang=en - /pictures/view_all_tags/{page}?search_text={name}&lang=en - - - - ]*>\s*]*">(?[^<]+)<\/a>\s*(?[^<]+)<\/span>\s*<\/li>]]> - ]*data-pubtime="(?[^"]+)">\s*]*>\s*\s*]*>\s*[^"]+)"[^>]*>]]> - \s*(?\d+)<\/td>\s*\s*(?.+?)<\/a>.*?<\/td>\s*.*?<\/td>\s*.*?<\/td>\s*(?.+?)<\/td>\s*(?\d+)<\/td>\s*<\/tr>]]> - page of (\d+) - - - / - mailto:stalkerg@gmail\.com - - 0 - - lower - - - diff --git a/release/sites/Booru-on-rails/model.ts b/release/sites/Booru-on-rails/model.ts index 5bd6e84c7..597eab8e5 100644 --- a/release/sites/Booru-on-rails/model.ts +++ b/release/sites/Booru-on-rails/model.ts @@ -1,19 +1,19 @@ -function completeImage(img: IImage): IImage { - if (img["json_uris"]) { - const uris = JSON.parse(img["json_uris"].replace(/"/g, '"')); +function completeImage(img: IImage & { json_uris: string }): IImage { + if (img.json_uris) { + const uris = JSON.parse(img.json_uris.replace(/"/g, '"')); if ("thumb_small" in uris && uris["thumb_small"].length > 5) { - img["preview_url"] = uris["thumb_small"]; + img.preview_url = uris["thumb_small"]; } if ("large" in uris && uris["large"].length > 5) { - img["sample_url"] = uris["large"]; + img.sample_url = uris["large"]; } if ("full" in uris && uris["full"].length > 5) { - img["file_url"] = uris["full"]; + img.file_url = uris["full"]; } } - if (!img["preview_url"] && img["file_url"].length >= 5) { - img["preview_url"] = img["file_url"] + if (!img.preview_url && img.file_url.length >= 5) { + img.preview_url = img.file_url .replace("full", "thumb") .replace(".svg", ".png"); } @@ -21,18 +21,6 @@ function completeImage(img: IImage): IImage { return img; } -const auth: { [id: string]: IAuth } = { - url: { - type: "url", - fields: [ - { - key: "key", - type: "password", - }, - ], - }, -}; - export const source: ISource = { name: "Booru-on-rails", modifiers: ["faved_by:", "width:", "height:", "uploader:", "source_url:", "description:", "sha512_hash:", "aspect_ratio:"], @@ -47,7 +35,18 @@ export const source: ISource = { parenthesis: true, precedence: "and", }, - auth, + auth: { + url: { + type: "url", + fields: [ + { + id: "apiKey", + key: "key", + type: "password", + }, + ], + }, + }, apis: { json: { name: "JSON", @@ -55,12 +54,10 @@ export const source: ISource = { forcedLimit: 15, search: { url: (query: any, opts: any, previous: any): string => { - opts["auth"]["key"] = opts["auth"]["password"]; - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); if (!query.search || query.search.length === 0) { - return "/images.json?" + loginPart + "page=" + query.page + "&nocomments=1&nofav=1"; + return "/images.json?page=" + query.page + "&nocomments=1&nofav=1"; } - return "/search.json?" + loginPart + "page=" + query.page + "&q=" + encodeURIComponent(query.search) + "&nocomments=1&nofav=1"; + return "/search.json?page=" + query.page + "&q=" + encodeURIComponent(query.search) + "&nocomments=1&nofav=1"; }, parse: (src: string): IParsedSearch => { const map = { @@ -82,11 +79,11 @@ export const source: ISource = { const images: IImage[] = []; for (const image of results) { const img = Grabber.mapFields(image, map); - img["tags"] = image["tags"].split(", "); - img["preview_url"] = image["representations"]["thumb"]; - img["sample_url"] = image["representations"]["large"]; - img["file_url"] = image["representations"]["full"]; - img["has_comments"] = image["comment_count"] > 0; + img.tags = image["tags"].split(", "); + img.preview_url = image["representations"]["thumb"]; + img.sample_url = image["representations"]["large"]; + img.file_url = image["representations"]["full"]; + img.has_comments = image["comment_count"] > 0; images.push(completeImage(img)); } @@ -98,9 +95,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - opts["auth"]["key"] = opts["auth"]["password"]; - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tags.json?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tags.json?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { const map = { @@ -127,12 +122,10 @@ export const source: ISource = { forcedLimit: 15, search: { url: (query: any, opts: any, previous: any): string => { - opts["auth"]["key"] = opts["auth"]["password"]; - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); if (!query.search || query.search.length === 0) { - return "/images/page/" + query.page + "?" + loginPart; + return "/images/page/" + query.page; } - return "/search?" + loginPart + "page=" + query.page + "&sbq=" + encodeURIComponent(query.search); + return "/search?page=" + query.page + "&sbq=" + encodeURIComponent(query.search); }, parse: (src: string): IParsedSearch => { return { @@ -154,9 +147,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - opts["auth"]["key"] = opts["auth"]["password"]; - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tags?" + loginPart + "page=" + query.page; + return "/tags?page=" + query.page; }, parse: (src: string): IParsedTags => { return { diff --git a/release/sites/Booru-on-rails/model.xml b/release/sites/Booru-on-rails/model.xml deleted file mode 100644 index 09627b0f5..000000000 --- a/release/sites/Booru-on-rails/model.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - Booru-on-rails - - full->thumb&.svg->.png - - /images.json?key={password}&page={page}&nocomments=1&nofav=1 - /search.json?key={password}&page={page}&q={tags}&nocomments=1&nofav=1 - 15 - /tags.json?key={password}&page={page} - /tags.json?key={password}&page={page}&tq={name} - - - /images/page/{page}?key={password} - /search?key={password}&page={page}&sbq={tags} - /{id} - 15 - /tags?key={password}&page={page} - /tags?key={password}&page={page}&tq={name} - - - - [^"]*)")? data-tag-id="(?[^"]+)" data-tag-name="(?[^"]+)" data-tag-slug="[^"]+">]]> - [^"]*)" data-created-at="(?[^"]*)" data-download-uri="(?[^"]*)" data-downvotes="[^"]*" data-faves="(?[^"]*)" data-height="(?[^"]*)" data-image-id="(?[^"]*)" data-image-tag-aliases="(?[^"]*)" data-image-tags="[^"]*" data-orig-sha512="[^"]*" data-score="(?[^"]*)" data-sha512="(?[^"]*)" data-size="[^"]*" data-source-url="(?[^"]*)" data-upvotes="[^"]*" data-uris="[^"]*" data-width="(?[^"]*)">.*?]*>]* src="(?[^"]*)"\/><\/a><\/div>]]> - [^"]+)")? data-tag-id="(?\d+)" data-tag-name="(?.+?)".+?\s*\((?\d+)\)<\/span>]]> - Last]]> - ([^<]+)<\/strong> total]]> - - - / - Powered by the booru-on-rails project - - faved_by: width: height: uploader: source_url: description: sha512_hash: aspect_ratio: - 1 - - lower - - - diff --git a/release/sites/Danbooru (2.0)/model.ts b/release/sites/Danbooru (2.0)/model.ts index c106f6ecd..d1d872d09 100644 --- a/release/sites/Danbooru (2.0)/model.ts +++ b/release/sites/Danbooru (2.0)/model.ts @@ -1,64 +1,28 @@ function completeImage(img: IImage): IImage { - if (img["ext"] && img["ext"][0] === ".") { - img["ext"] = img["ext"].mid(1); + if (img.ext && img.ext[0] === ".") { + img.ext = img.ext.substr(1); } - if (!img["file_url"] || img["file_url"].length < 5) { - img["file_url"] = `/data/${img["md5"]}.${img["ext"] || "jpg"}`; + if (!img.file_url || img.file_url.length < 5) { + img.file_url = `/data/${img.md5}.${img.ext || "jpg"}`; } else { - img["file_url"] = img["file_url"] + img.file_url = img.file_url .replace("/preview/", "/") .replace("/ssd/", "/") .replace("/sample/[^.]*sample-", "/"); } - if (!img["sample_url"] || img["sample_url"].length < 5) { - img["sample_url"] = `/data/sample/sample-${img["md5"]}.jpg`; + if (!img.sample_url || img.sample_url.length < 5) { + img.sample_url = `/data/sample/sample-${img.md5}.jpg`; } - if (!img["preview_url"] || img["preview_url"].length < 5) { - img["preview_url"] = `/data/preview/${img["md5"]}.jpg`; + if (!img.preview_url || img.preview_url.length < 5) { + img.preview_url = `/data/preview/${img.md5}.jpg`; } return img; } -const auth: { [id: string]: IAuth } = { - url: { - type: "url", - fields: [ - { - key: "login", - type: "username", - }, - { - key: "password_hash", - type: "hash", - hash: "sha1", - salt: "choujin-steiner--%value%--", - }, - ], - }, - session: { - type: "post", - url: "/session", - fields: [ - { - key: "name", - type: "username", - }, - { - key: "password", - type: "password", - }, - ], - check: { - type: "cookie", - key: "password_hash", - }, - }, -}; - export const source: ISource = { name: "Danbooru (2.0)", modifiers: ["rating:safe", "rating:questionable", "rating:explicit", "rating:s", "rating:q", "rating:e", "user:", "fav:", "fastfav:", "md5:", "source:", "id:", "width:", "height:", "score:", "mpixels:", "filesize:", "date:", "gentags:", "arttags:", "chartags:", "copytags:", "approver:", "parent:", "sub:", "status:any", "status:deleted", "status:active", "status:flagged", "status:pending", "order:id", "order:id_desc", "order:score", "order:score_asc", "order:mpixels", "order:mpixels_asc", "order:filesize", "order:landscape", "order:portrait", "order:favcount", "order:rank", "order:change", "order:change_desc", "parent:none", "unlocked:rating"], @@ -76,23 +40,62 @@ export const source: ISource = { parenthesis: false, precedence: "or", }, - auth, + auth: { + url: { + type: "url", + fields: [ + { + id: "pseudo", + key: "login", + }, + { + id: "password", + type: "password", + }, + { + key: "password_hash", + type: "hash", + hash: "sha1", + salt: "choujin-steiner--%password%--", + }, + ], + }, + session: { + type: "post", + url: "/session", + fields: [ + { + id: "pseudo", + key: "name", + }, + { + id: "password", + key: "password", + type: "password", + }, + ], + check: { + type: "cookie", + key: "password_hash", + }, + }, + }, apis: { json: { name: "JSON", auth: [], maxLimit: 200, search: { + parseErrors: true, url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 1000, "{page}", "a{max}", "b{min}"); - return "/posts.json?" + loginPart + "limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); + return "/posts.json?limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } }, - parse: (src: string): IParsedSearch => { + parse: (src: string): IParsedSearch | IError => { const map = { "created_at": "created_at", "status": "status", @@ -128,10 +131,14 @@ export const source: ISource = { const data = JSON.parse(src); + if ("success" in data && data["success"] === false && "message" in data) { + return { error: data["message"] }; + } + const images: IImage[] = []; for (const image of data) { const img = Grabber.mapFields(image, map); - if (!img["md5"] || img["md5"].length === 0) { + if (!img.md5 || img.md5.length === 0) { continue; } images.push(completeImage(img)); @@ -142,8 +149,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tags.json?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tags.json?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { const map = { @@ -151,13 +157,18 @@ export const source: ISource = { "name": "name", "count": "post_count", "typeId": "category", + "related": "related_tags", }; const data = JSON.parse(src); const tags: ITag[] = []; for (const tag of data) { - tags.push(Grabber.mapFields(tag, map)); + const ret = Grabber.mapFields(tag, map); + if (ret.related) { + ret.related = ret.related.split(" ").filter((_: string, i: number) => i % 2 === 0); + } + tags.push(ret); } return { tags }; @@ -169,16 +180,16 @@ export const source: ISource = { auth: [], maxLimit: 200, search: { + parseErrors: true, url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 1000, "{page}", "a{max}", "b{min}"); - return "/posts.xml?" + loginPart + "limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); + return "/posts.xml?limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } }, - parse: (src: string): IParsedSearch => { + parse: (src: string): IParsedSearch | IError => { const map = { "created_at": "created-at", "status": "status", @@ -212,12 +223,17 @@ export const source: ISource = { "tags_meta": "tag-string-meta", }; - const data = Grabber.makeArray(Grabber.typedXML(Grabber.parseXML(src)).posts.post); + const xml = Grabber.parseXML(src); + + if ("result" in xml && "@attributes" in xml["result"] && "success" in xml["result"]["@attributes"] && xml["result"]["@attributes"]["success"] === "false") { + return { error: xml["result"]["#text"] }; + } + const data = Grabber.makeArray(Grabber.typedXML(xml).posts.post); const images: IImage[] = []; for (const image of data) { const img = Grabber.mapFields(image, map); - if (!img["md5"] || img["md5"].length === 0) { + if (!img.md5 || img.md5.length === 0) { continue; } images.push(completeImage(img)); @@ -228,8 +244,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tags.xml?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tags.xml?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { const map = { @@ -237,13 +252,18 @@ export const source: ISource = { "name": "name", "count": "post-count", "typeId": "category", + "related": "related-tags", }; const data = Grabber.makeArray(Grabber.typedXML(Grabber.parseXML(src)).tags.tag); const tags: ITag[] = []; for (const tag of data) { - tags.push(Grabber.mapFields(tag, map)); + const ret = Grabber.mapFields(tag, map); + if (ret.related) { + ret.related = ret.related.split(" ").filter((_: string, i: number) => i % 2 === 0); + } + tags.push(ret); } return { tags }; @@ -255,16 +275,21 @@ export const source: ISource = { auth: [], maxLimit: 200, search: { + parseErrors: true, url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 1000, "{page}", "a{max}", "b{min}"); - return "/posts?" + loginPart + "limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); + return "/posts?limit=" + opts.limit + "&page=" + pagePart + "&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } }, - parse: (src: string): IParsedSearch => { + parse: (src: string, statusCode: number): IParsedSearch | IError => { + const match = src.match(/
\s*

([^<]+)<\/p>\s*<\/div>/m); + if (match) { + return { error: match[1] }; + } + let wiki = Grabber.regexToConst("wiki", '

]+)>(?.+?)
', src); wiki = wiki ? wiki.replace(/href="\/wiki_pages\/show_or_new\?title=([^"]+)"/g, 'href="$1"') : wiki; return { @@ -289,8 +314,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tags?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tags?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { return { diff --git a/release/sites/Danbooru (2.0)/model.xml b/release/sites/Danbooru (2.0)/model.xml deleted file mode 100644 index 941a9d966..000000000 --- a/release/sites/Danbooru (2.0)/model.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - Danbooru (2.0) - - /data/{md5}.{ext} - /preview/->/&/ssd/->/&/sample/[^.]*sample-->/ - /data/sample/sample-{md5}.jpg - /data/preview/{md5}.jpg - - login={pseudo}&password_hash={password}& - /posts.xml?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts.xml?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - 200 - 1000 - {page} - a{max} - b{min} - /tags.xml?{login}limit={limit}&page={page} - /tags.xml?{login}limit={limit}&page={page}&search[name]={name} - - - login={pseudo}&password_hash={password}& - /posts.json?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts.json?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - 200 - 1000 - {page} - a{max} - b{min} - /tags.json?{login}limit={limit}&page={page} - /tags.json?{login}limit={limit}&page={page}&search[name]={name} - - - login={pseudo}&password_hash={password}& - /posts?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - /posts/{id} - 1000 - {page} - a{max} - b{min} - /tags?{login}limit={limit}&page={page} - /tags?{login}limit={limit}&page={page}&search[name]={name} - - - - - URL - - - pseudo - username - - - password_hash - hash - sha1 - choujin-steiner--%value%-- - - - - - Post - /session - - - name - username - - - password - password - - - - password_hash - - - - - [^\"]+)">(?:\s*
\?<\/a>)?\s*]*href="[^\"]+\"[^>]*>(?[^<]+)<\/a>\s*(?[^<]+)<\/span>\s*<\/li>]]> - ]* id="[^"]*" class="[^"]*"\s+data-id="(?[^"]*)"\s+data-has-sound="[^"]*"\s+data-tags="(?[^"]*)"\s+data-pools="(?[^"]*)"\s+data-uploader="(?[^"]*)"\s+data-approver-id="(?[^"]*)"\s+data-rating="(?[^"]*)"\s+data-width="(?[^"]*)"\s+data-height="(?[^"]*)"\s+data-flags="(?[^"]*)"\s+data-parent-id="(?[^"]*)"\s+data-has-children="(?[^"]*)"\s+data-score="(?[^"]*)"\s+data-views="[^"]*"\s+data-fav-count="(?[^"]*)"\s+data-pixiv-id="[^"]*"\s+data-file-ext="(?[^"]*)"\s+data-source="[^"]*"\s+data-normalized-source="[^"]*"\s+data-is-favorited="[^"]*"\s+data-md5="(?[^"]*)"\s+data-file-url="(?[^"]*)"\s+data-large-file-url="(?[^"]*)"\s+data-preview-file-url="(?[^"]*)"]]> - ]*>\s*]*>(?\d+)<\/td>\s*\s*]+>\?<\/a>\s*]+>(?.+?)<\/a>\s*<\/td>\s*]*>\s*(?:)?]]> - - ([0-9]+)<\/a><\/li>]* rel="next"]]> - [^<]*Pool:[^<]*(?:<<)?[^<]*([^<]+)[^<]*(?:>>)?[^<]*
]]> - ]+)>(.+?)]]> - true - - - / - Running Danbooru v2|Running Danbooru <a[^>]*>v2 - - rating:safe rating:questionable rating:explicit rating:s rating:q rating:e user: fav: fastfav: md5: source: id: width: height: score: mpixels: filesize: date: gentags: arttags: chartags: copytags: approver: parent: sub: status:any status:deleted status:active status:flagged status:pending order:id order:id_desc order:score order:score_asc order:mpixels order:mpixels_asc order:filesize order:landscape order:portrait order:favcount order:rank order:change order:change_desc parent:none unlocked:rating - 1 - choujin-steiner--%password%-- - - lower - _ - - diff --git a/release/sites/Danbooru/model.ts b/release/sites/Danbooru/model.ts index c61135b0d..c57863513 100644 --- a/release/sites/Danbooru/model.ts +++ b/release/sites/Danbooru/model.ts @@ -28,42 +28,6 @@ const completeImage = (data: any): IImage => { return data; }; -const auth: { [id: string]: IAuth } = { - url: { - type: "url", - fields: [ - { - key: "login", - type: "username", - }, - { - key: "password_hash", - type: "hash", - hash: "sha1", - salt: "choujin-steiner--%value%--", - }, - ], - }, - session: { - type: "post", - url: "/user/authenticate", - fields: [ - { - key: "user[name]", - type: "username", - }, - { - key: "user[password]", - type: "password", - }, - ], - check: { - type: "cookie", - key: "pass_hash", - }, - }, -}; - export const source: ISource = { name: "Danbooru", modifiers: ["rating:safe", "rating:questionable", "rating:explicit", "user:", "fav:", "fastfav:", "md5:", "source:", "id:", "width:", "height:", "score:", "mpixels:", "filesize:", "date:", "gentags:", "arttags:", "chartags:", "copytags:", "approver:", "parent:", "sub:", "status:any", "status:deleted", "status:active", "status:flagged", "status:pending", "order:id", "order:id_desc", "order:score", "order:score_asc", "order:mpixels", "order:mpixels_asc", "order:filesize", "order:landscape", "order:portrait", "order:favcount", "order:rank", "order:change", "order:change_desc", "parent:none", "unlocked:rating"], @@ -80,7 +44,46 @@ export const source: ISource = { parenthesis: false, precedence: "or", }, - auth, + auth: { + url: { + type: "url", + fields: [ + { + id: "pseudo", + key: "login", + }, + { + id: "password", + type: "password", + }, + { + key: "password_hash", + type: "hash", + hash: "sha1", + salt: "choujin-steiner--%password%--", + }, + ], + }, + session: { + type: "post", + url: "/user/authenticate", + fields: [ + { + id: "pseudo", + key: "user[name]", + }, + { + id: "password", + key: "user[password]", + type: "password", + }, + ], + check: { + type: "cookie", + key: "pass_hash", + }, + }, + }, apis: { json: { name: "JSON", @@ -89,9 +92,8 @@ export const source: ISource = { search: { url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 750, "page={page}", "after_id={max}", "before_id={min}"); - return "/post/index.json?" + loginPart + "limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); + return "/post/index.json?limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } @@ -109,8 +111,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tag/index.json?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tag/index.json?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { const map = { @@ -138,9 +139,8 @@ export const source: ISource = { search: { url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 750, "page={page}", "after_id={max}", "before_id={min}"); - return "/post/index.xml?" + loginPart + "limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); + return "/post/index.xml?limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } @@ -163,8 +163,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tag/index.xml?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tag/index.xml?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { const map = { @@ -193,9 +192,8 @@ export const source: ISource = { search: { url: (query: any, opts: any, previous: any): string | IError => { try { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); const pagePart = Grabber.pageUrl(query.page, previous, 750, "page={page}", "after_id={max}", "before_id={min}"); - return "/post/index?" + loginPart + "limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); + return "/post/index?limit=" + opts.limit + "&" + pagePart + "&typed_tags=true&tags=" + encodeURIComponent(query.search); } catch (e) { return { error: e.message }; } @@ -225,8 +223,7 @@ export const source: ISource = { }, tags: { url: (query: any, opts: any): string => { - const loginPart = Grabber.loginUrl(auth.url.fields, opts["auth"]); - return "/tag/index?" + loginPart + "limit=" + opts.limit + "&page=" + query.page; + return "/tag/index?limit=" + opts.limit + "&page=" + query.page; }, parse: (src: string): IParsedTags => { return { diff --git a/release/sites/Danbooru/model.xml b/release/sites/Danbooru/model.xml deleted file mode 100644 index 7381171e3..000000000 --- a/release/sites/Danbooru/model.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - Danbooru - - /preview/->/ - - login={pseudo}&password_hash={password}& - /post/index.xml?{login}limit={limit}&{cpage}&typed_tags=true&tags={tags} - /post/index.xml?{login}limit={limit}&{cpage}&typed_tags=true&tags=pool:{pool} {tags} - 200 - 750 - page={page} - after_id={max} - before_id={min} - /tag/index.xml?{login}limit={limit}&page={page} - /tag/index.xml?{login}limit={limit}&page={page}&name={name} - - - login={pseudo}&password_hash={password}& - /post/index.json?{login}limit={limit}&{cpage}&typed_tags=true&tags={tags} - /post/index.json?{login}limit={limit}&{cpage}&typed_tags=true&tags=pool:{pool} {tags} - 200 - 750 - page={page} - after_id={max} - before_id={min} - /tag/index.json?{login}limit={limit}&page={page} - /tag/index.json?{login}limit={limit}&page={page}&name={name} - - - login={pseudo}&password_hash={password}& - /post/index?{login}limit={limit}&{cpage}&tags={tags} - /post/index?{login}limit={limit}&{cpage}&tags=pool:{pool} {tags} - /post/show/{id} - 750 - page={page} - after_id={max} - before_id={min} - /tag/index?{login}limit={limit}&page={page} - /tag/index?{login}limit={limit}&page={page}&name={name} - - - - - URL - - - pseudo - username - - - password - hash - sha1 - choujin-steiner--%password%-- - - - - - Post - /user/authenticate - - - user[name] - username - - - user[password] - password - - - - pass_hash - - - - - ]*tag-type-(?[^">]+)(?:|"[^>]*)>.*?]*>(?[^<\?]+)<\/a>.*?(?\d+)<\/span>.*?<\/li>]]> - \{.+?\})\);?]]> - ]*>\s*]*>(?\d+)<\/td>\s*]*>\s*(?:]+>\?<\/a>\s*)?]+>(?.+?)<\/a>\s*<\/td>\s*]*>\s*(?.+?)\s*\([^()]+\)\s*<\/td>]]> - ]]> - [^<]*Pool:[^<]*(?:<<)?[^<]*([^<]+)[^<]*(?:>>)?[^<]*]]> - ]+)>(.+?)]]> - - - / - Running Danbooru 1|Running MyImouto 1 - - rating:safe rating:questionable rating:explicit user: fav: fastfav: md5: source: id: width: height: score: mpixels: filesize: date: gentags: arttags: chartags: copytags: approver: parent: sub: status:any status:deleted status:active status:flagged status:pending order:id order:id_desc order:score order:score_asc order:mpixels order:mpixels_asc order:filesize order:landscape order:portrait order:favcount order:rank order:change order:change_desc parent:none unlocked:rating - 1 - choujin-steiner--%password%-- - - lower - _ - - diff --git a/release/sites/E-Hentai/model.ts b/release/sites/E-Hentai/model.ts index e1b6de59e..bbe2cb5f7 100644 --- a/release/sites/E-Hentai/model.ts +++ b/release/sites/E-Hentai/model.ts @@ -20,27 +20,6 @@ function sizeToInt(size: string): number { return val; } -const auth: { [id: string]: IAuth } = { - post: { - type: "post", - url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01", - fields: [ - { - key: "UserName", - type: "username", - }, - { - key: "PassWord", - type: "password", - }, - ], - check: { - type: "cookie", - key: "ipb_member_id", - }, - }, -}; - export const source: ISource = { name: "E-Hentai", modifiers: [], @@ -48,7 +27,27 @@ export const source: ISource = { searchFormat: { and: " ", }, - auth, + auth: { + post: { + type: "post", + url: "https://forums.e-hentai.org/index.php?act=Login&CODE=01", + fields: [ + { + id: "pseudo", + key: "UserName", + }, + { + id: "password", + key: "PassWord", + type: "password", + }, + ], + check: { + type: "cookie", + key: "ipb_member_id", + }, + }, + }, apis: { html: { name: "Regex", @@ -59,8 +58,7 @@ export const source: ISource = { return "/?page=" + (query.page - 1) + "&f_search=" + encodeURIComponent(query.search); }, parse: (src: string): IParsedSearch => { - // Gallery mode regex:
]*>
(?[^<]+)<\/a><\/div>
]*>]*>]*><\/a><\/div>
]*>
]*>(?[^<]*)
', src); + const matches = Grabber.regexMatches(']*>]*>]*>(?[^<]*)
]*>]* id="i(?\\d+)"[^>]*>(?:]*>|(?[^<]*))
]*>(?[^<]+)
.+?
(?[^<]+).+?]+>(?[^<]+)', src); const images = matches.map((match: any) => { match["type"] = "gallery"; if ("encoded_thumbnail" in match && match["encoded_thumbnail"].length > 0) { @@ -78,13 +76,13 @@ export const source: ISource = { return { images, pageCount: Grabber.countToInt(Grabber.regexToConst("page", ">(?[0-9,]+)]*>(?:>|]*>>)", src)), - imageCount: Grabber.countToInt(Grabber.regexToConst("count", ">Showing \\d+-\\d+ of (?[0-9,]+)<", src)), + imageCount: Grabber.countToInt(Grabber.regexToConst("count", ">Showing page \\d+ of (?[0-9,]+) results<", src)), }; }, }, gallery: { url: (query: any, opts: any): string => { - return "/g/" + query.id + "/?p=" + (query.page - 1); + return "/g/" + query.md5 + "/?p=" + (query.page - 1); }, parse: (src: string): IParsedGallery => { const images: IImage[] = []; diff --git a/release/sites/E-Hentai/model.xml b/release/sites/E-Hentai/model.xml deleted file mode 100644 index 4efa6d74d..000000000 --- a/release/sites/E-Hentai/model.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - E-Hentai - - - login={pseudo}&password_hash={password}& - /posts.xml?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts.xml?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - 200 - 1000 - {page} - a{max} - b{min} - /tags.xml?{login}limit={limit}&page={page} - /tags.xml?{login}limit={limit}&page={page}&search[name]={name} - - - login={pseudo}&password_hash={password}& - /posts.json?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts.json?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - 200 - 1000 - {page} - a{max} - b{min} - /tags.json?{login}limit={limit}&page={page} - /tags.json?{login}limit={limit}&page={page}&search[name]={name} - - - login={pseudo}&password_hash={password}& - /posts?{login}limit={limit}&page={pagepart}{altpage}&tags={tags} - /posts?{login}limit={limit}&page={pagepart}{altpage}&tags=pool:{pool} {tags} - /posts/{id} - 1000 - {page} - a{max} - b{min} - /tags?{login}limit={limit}&page={page} - /tags?{login}limit={limit}&page={page}&search[name]={name} - - - - / - Running Danbooru v2|Running Danbooru <a[^>]*>v2 - - 0 - diff --git a/release/sites/FurAffinity/icon.png b/release/sites/FurAffinity/icon.png new file mode 100644 index 000000000..9829da0bb Binary files /dev/null and b/release/sites/FurAffinity/icon.png differ diff --git a/release/sites/FurAffinity/model.ts b/release/sites/FurAffinity/model.ts new file mode 100644 index 000000000..ff3099cc4 --- /dev/null +++ b/release/sites/FurAffinity/model.ts @@ -0,0 +1,52 @@ +export const source: ISource = { + name: "FurAffinity", + tagFormat: { + case: "lower", + wordSeparator: "_", + }, + searchFormat: { + and: " ", + or: " | ", + parenthesis: true, + precedence: "or", + }, + apis: { + html: { + name: "Regex", + auth: [], + forcedLimit: 24, + search: { + parseErrors: true, + url: (query: any, opts: any, previous: any): string | IError => { + return "/search/?q=" + encodeURIComponent(query.search) + "&order-by=date&page=" + query.page + "&perpage=24"; + }, + parse: (src: string, statusCode: number): IParsedSearch | IError => { + return { + images: Grabber.regexToImages('
[^"]+)">.+?[^"]+)"\\s*data-width="(?[0-9.]+)"\\s*data-height="(?[0-9.]+)"', src), + imageCount: Grabber.regexToConst("count", "Search results \\(\\d+ - \\d+ of (?\\d+)\\)", src), + }; + }, + }, + details: { + url: (id: number, md5: string): string => { + return "/view/" + id + "/"; + }, + parse: (src: string): IParsedDetails => { + return { + tags: Grabber.regexToTags('(?[^<]+)', src), + createdAt: Grabber.regexToConst("date", 'Posted: ', src), + imageUrl: Grabber.regexToConst("url", 'Download', src), + }; + }, + }, + check: { + url: (): string => { + return "/"; + }, + parse: (src: string): boolean => { + return src.indexOf("Fur Affinity is ©") !== -1; + }, + }, + }, + }, +}; diff --git a/release/sites/FurAffinity/sites.txt b/release/sites/FurAffinity/sites.txt new file mode 100644 index 000000000..e69de29bb diff --git a/release/sites/FurAffinity/www.furaffinity.net/defaults.ini b/release/sites/FurAffinity/www.furaffinity.net/defaults.ini new file mode 100644 index 000000000..2d7019f94 --- /dev/null +++ b/release/sites/FurAffinity/www.furaffinity.net/defaults.ini @@ -0,0 +1,4 @@ +[General] +name=FurAffinity +ssl=true +headers="@Variant(\0\0\0\b\0\0\0\x1\0\0\0\x14\0U\0s\0\x65\0r\0-\0\x41\0g\0\x65\0n\0t\0\0\0\f\0\0\0NMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:52.0) Gecko/20100101 Firefox/52.0)" diff --git a/release/sites/Gelbooru (0.1)/model.ts b/release/sites/Gelbooru (0.1)/model.ts index 37c6b13d9..70f451ca7 100644 --- a/release/sites/Gelbooru (0.1)/model.ts +++ b/release/sites/Gelbooru (0.1)/model.ts @@ -1,6 +1,6 @@ function completeImage(img: IImage): IImage { - if (!img["file_url"] || img["file_url"].length < 5) { - img["file_url"] = img["preview_url"] + if (!img.file_url || img.file_url.length < 5) { + img.file_url = img.preview_url .replace("/thumbnails/", "/images/") .replace("/thumbnail_", "/"); } diff --git a/release/sites/Gelbooru (0.1)/model.xml b/release/sites/Gelbooru (0.1)/model.xml deleted file mode 100644 index bcbcebf83..000000000 --- a/release/sites/Gelbooru (0.1)/model.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - Gelbooru (0.1) - - /thumbnails/->/images/&/thumbnail_->/ - - /index.php?page=post&s=list&limit={limit}&pid={pid}&tags={tags} - /index.php?page=pool&s=show&id={pool} - /index.php?page=post&s=view&id={id} - - - - ]*>\+]*>- ]*>\? (?[^<]+) (?\d+)]]> - [^.]+)\.[^"]+)" alt="post" border="0" title=" *(?[^"]*) *score:(?[^ "]+) *rating:(?[^ "]+) *"\/><\/a>[^<]*(?: +

Front Page Torrents Favorites My Home My Galleries Toplists Bounties News Forums Wiki HentaiVerse

+ +

[MUGENKIDOU A (Tomose Shunsaku)] Enko-sei!+ [Digital] [Textless]

[無限軌道A (トモセシュンサク)] えんこーせい!+ [DL版] [無字]

Free Hentai Doujinshi Gallery: [MUGENKIDOU A (Tomose Shunsaku)] Enko-sei!+ [Digital] [Textless]
Posted:2018-10-07 23:24
Parent:None
Visible:Yes
Language:N/A  
File Size:43.01 MB
Length:31 pages
Favorited:168 times
Rating:
57
Average: 4.69

Showing 1 - 31 of 31 images

<1>
4 rows
10 rows
20 rows
40 rows
Normal
Large
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<1>
Posted on 07 October 2018, 23:24 UTC by:   ZedsShadow    PM
Uploader Comment
+ + +
+

[Front Page]

+
Please read the Terms of Service before participating with or uploading any content to this site.
+ + diff --git a/tests/resources/pages/e-hentai.org/pack-loader-gallery-2-1.html b/tests/resources/pages/e-hentai.org/pack-loader-gallery-2-1.html new file mode 100644 index 000000000..f916d6bd5 --- /dev/null +++ b/tests/resources/pages/e-hentai.org/pack-loader-gallery-2-1.html @@ -0,0 +1,49 @@ + + + +[MUGENKIDOU A (Tomose Shunsaku)] Enko-sei!+ [English] [Hentai_Doctor] [Digital] - E-Hentai Galleries + + + + + + + + + + + + +

Front Page Torrents Favorites My Home My Galleries Toplists Bounties News Forums Wiki HentaiVerse

+ +

[MUGENKIDOU A (Tomose Shunsaku)] Enko-sei!+ [English] [Hentai_Doctor] [Digital]

[無限軌道A (トモセシュンサク)] えんこーせい!+ [英訳] [DL版]

Showing 1 - 32 of 32 images

<1>
4 rows
10 rows
20 rows
40 rows
Normal
Large
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<1>
Posted on 07 October 2018, 19:24 UTC by:   ZedsShadow    PM
Uploader Comment
Original: https://e-hentai.org/g/1232785/2160020c65/

Please let me know if I've made any mistakes!

Enjoy!
Posted on 07 October 2018, 20:24 UTC by:   Hakrei    PM
Score +58
Awesome, thanks for the translation of the colored version
Any chances you'll consider doing this one by the same artist? https://e-hentai.org/g/1017546/7fb01e75a0/
Posted on 08 October 2018, 03:57 UTC by:   neonsunkeitaro    PM
Score +7
My wish finally granted !! thank !! LoL
Posted on 09 October 2018, 23:54 UTC by:   illmatic27    PM
Score +15
one of my favorites just got a whole lot better
+ + +
+

[Front Page]

+
Please read the Terms of Service before participating with or uploading any content to this site.
+ + diff --git a/tests/resources/pages/e-hentai.org/pack-loader-gallery-3-1.html b/tests/resources/pages/e-hentai.org/pack-loader-gallery-3-1.html new file mode 100644 index 000000000..1c96a81a2 --- /dev/null +++ b/tests/resources/pages/e-hentai.org/pack-loader-gallery-3-1.html @@ -0,0 +1,49 @@ + + + +(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [French] [Zer0] - E-Hentai Galleries + + + + + + + + + + + + +

Front Page Torrents Favorites My Home My Galleries Toplists Bounties News Forums Wiki HentaiVerse

+ +

(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [French] [Zer0]

(C94) [無限軌道A (トモセシュンサク)] コスぱこ! [フランス翻訳]

Free Hentai Doujinshi Gallery: (C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [French] [Zer0]
Posted:2018-10-07 09:39
Parent:None
Visible:Yes
Language:French  TR
File Size:20.44 MB
Length:24 pages
Favorited:139 times
Rating:
30
Average: 4.61

Showing 1 - 24 of 24 images

<1>
4 rows
10 rows
20 rows
40 rows
Normal
Large
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<1>
Posted on 07 October 2018, 09:39 UTC by:   mangakam    PM
Uploader Comment
Merci à Zer01967 qui m'a donné son accord pour poster ses traductions.

Vous pouvez découvrir ses autres traductions via son blog : http://zer0-hentai.over-blog.com/
+ + +
+

[Front Page]

+
Please read the Terms of Service before participating with or uploading any content to this site.
+ + diff --git a/tests/resources/pages/e-hentai.org/pack-loader-list.html b/tests/resources/pages/e-hentai.org/pack-loader-list.html new file mode 100644 index 000000000..da6fcbbad --- /dev/null +++ b/tests/resources/pages/e-hentai.org/pack-loader-list.html @@ -0,0 +1,34 @@ + + + + +E-Hentai Galleries - The Free Hentai Doujinshi, Manga and Image Gallery System + + + + + + + + + + + +
+
+

E-Hentai Galleries: The Free Hentai Doujinshi, Manga and Image Gallery System

+ + + + +

Showing page 1 of 211 results

<1234567...9>
PublishedTitleUploader
Doujinshi
(C95) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 9 [Portuguese-BR] (DiegoVPR)
2019-03-29 16:20
T
portuguese
translated
original
f:big breasts
f:blowjob
f:bunny girl
f:dark skin
f:glasses
f:leotard
f:nakadashi
f:pantyhose
f:stockings
22 pages
Doujinshi
inits~ehgt.org~t/0b/7a/0b7ae5e43acbbf1f26ef417326dfecc0b7e837f7-2200705-2106-3000-jpg_250.jpg~(C95) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 9 [English] {Doujins.com}
2019-03-22 12:57
T
english
translated
original
f:big breasts
f:blowjob
f:bunny girl
f:dark skin
f:glasses
f:leotard
f:nakadashi
f:pantyhose
f:stockings
20 pages
Doujinshi
inits~ehgt.org~t/db/4a/db4aa9ad4a9fd2e571a1b382f040f6656b19eafb-309854-847-1200-jpg_250.jpg~(C91) [Mugenkidou A (Tomose Shunsaku)] Furarete Kuyashikatta node Shikatanaku Saimin de Kanojo ni Shitemimashita. [Portuguese-BR]
2019-02-26 17:50
T
portuguese
translated
original
f:big breasts
f:mind control
f:schoolgirl uniform
f:sole female
m:glasses
m:schoolboy uniform
m:sole male
mugenkidou a
tomose shunsaku
22 pages
Doujinshi
inits~ehgt.org~t/cd/0e/cd0e0a8de74eb6b57af83840e5a84aaee90c2345-268740-846-1200-jpg_250.jpg~(C92) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 8 (Dragon Quest XI) [Portuguese-BR]
2019-02-26 17:50
T
portuguese
translated
dragon quest xi
luminary
martina
f:ponytail
mugenkidou a
tomose shunsaku
full color
mosaic censorship
14 pages
Doujinshi
inits~ehgt.org~t/df/e6/dfe63354d64ffb20f94456c781138405ce20552a-2115712-1449-2031-jpg_250.jpg~(C84) [Mugenkidou A (Tomose Shunsaku)] Houkago Aimashou | Let's Meet After School [Portuguese-BR]
2019-02-24 17:42
T
portuguese
translated
f:defloration
f:schoolgirl uniform
f:sole female
m:sole male
mugenkidou a
tomose shunsaku
20 pages
Doujinshi
inits~ehgt.org~t/d2/93/d2936e99cf912433ecb94aa574e1959e6bd1b7e8-1293903-1392-2000-jpg_250.jpg~(C86) [Mugenkidou A (Tomose Shunsaku)] Natsukano | Summer Girlfriend [Portuguese-BR]
2019-02-20 03:39
T
portuguese
translated
original
f:bikini
f:nakadashi
f:sole female
f:twintails
m:sole male
mugenkidou a
tomose shunsaku
incest
27 pages
Doujinshi
inits~ehgt.org~t/df/e6/dfe6d149b2226f5791afc8efae1eea40dcf0d3c0-1629916-2560-3624-jpg_250.jpg~(C91) [Mugenkidou A (Tomose Shunsaku)] Furarete Kuyashikatta node Shikatanaku Saimin de Kanojo ni Shitemimashita. | Getting Rejected Hurt so I Had no Choice but to Try to Hypnotise Her. [Russian] [AgoniZ]
2019-01-29 15:35
T
russian
translated
original
f:impregnation
f:mind control
f:rape
f:schoolgirl uniform
f:sole female
m:glasses
m:schoolboy uniform
m:sole male
mugenkidou a
23 pages
Doujinshi
inits~ehgt.org~t/97/50/9750e8e34ae3c3818afb1465e34fabb502fd4165-2139326-2560-3624-jpg_250.jpg~(C91) [Mugenkidou A (Tomose Shunsaku)] Furarete Kuyashikatta node Shikatanaku Saimin de Kanojo ni Shitemimashita. | Getting Rejected Hurt so I Had no Choice but to Try to Hypnotise Her. [English] [pyonpyon]
2019-01-21 13:48
T
english
translated
original
f:big breasts
f:blowjob
f:defloration
f:impregnation
f:mind control
f:rape
f:schoolgirl uniform
f:sole female
m:schoolboy uniform
22 pages
Doujinshi
inits~ehgt.org~t/9b/0d/9b0db1acfad63439226941220bd5616d0d4f7cf0-1307509-2106-3000-jpg_250.jpg~(C95) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 9 | 무한 궤도 책! vol.9 [Korean]
2019-01-03 12:23
T
korean
translated
original
f:big breasts
f:dark skin
f:gyaru
f:stockings
f:twintails
m:sole male
mugenkidou a
tomose shunsaku
22 pages
Doujinshi
inits~ehgt.org~t/25/34/2534846ded63518fb677ad70bb7e43d0897470f2-1987448-2106-3000-jpg_250.jpg~(C95) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 9
2019-01-03 10:57
T
original
f:big breasts
f:blowjob
f:bunny girl
f:dark skin
f:glasses
f:gyaru
f:leotard
f:pantyhose
f:stockings
f:twintails
mugenkidou a
22 pages
Doujinshi
inits~ehgt.org~t/25/34/2534846ded63518fb677ad70bb7e43d0897470f2-1987448-2106-3000-jpg_250.jpg~(C95) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 9 [Chinese] [空気系☆漢化]
2019-01-03 09:55
T
chinese
translated
original
f:big breasts
f:blowjob
f:bunny girl
f:dark skin
f:glasses
f:gyaru
f:nakadashi
f:pantyhose
f:stockings
25 pages
Doujinshi
inits~ehgt.org~t/e5/ce/e5ce890a3c5ebceec43fa69f048d07d9ca127308-4004334-1688-2400-jpg_250.jpg~(COMIC1☆3) [Mugenkidou A (Tomose Shunsaku)] Wagamama Pet Life! [SPANISH] [ARKRAM] [Decensored]
2018-11-15 13:20
T
spanish
translated
original
f:big breasts
f:mind control
f:ponytail
f:rape
f:schoolgirl uniform
f:small breasts
m:schoolboy uniform
m:sole male
mugenkidou a
26 pages
Doujinshi
inits~ehgt.org~t/ce/2a/ce2a212b19f14c19f81c28005416a3ba9fa8450c-4821678-2120-3000-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [Chinese] [Mr.GUO 个人汉化]
2018-11-06 11:36
T
chinese
translated
original
f:ahegao
f:defloration
f:drunk
f:stockings
m:bbm
m:glasses
m:sole male
mugenkidou a
tomose shunsaku
24 pages
Doujinshi
inits~ehgt.org~t/8c/1c/8c1cee0d991df08ea6672100091c95cddf2d515f-580479-1200-1698-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [French] [Zer0]
2018-10-07 09:39
T
french
translated
original
f:ahegao
f:big breasts
f:cosplaying
f:defloration
f:drunk
f:nakadashi
f:spanking
f:stockings
f:x-ray
24 pages
Non-H
inits~ehgt.org~t/0d/b9/0db961c431eb7f558c55bd6364dfae88e00456ac-8425388-2509-3514-png_250.jpg~[Tomose Shunsaku] Tomose Shunsaku Potpourri Club Illustration Works
2018-08-21 05:45
T
f:bikini
tomose shunsaku
artbook
82 pages
Doujinshi
inits~ehgt.org~t/ce/2a/ce2a212b19f14c19f81c28005416a3ba9fa8450c-4821678-2120-3000-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! | Cosplay Girl! [English] [Hentai_Doctor]
2018-08-16 11:59
T
english
translated
original
f:ahegao
f:big breasts
f:cosplaying
f:defloration
f:drunk
f:nakadashi
f:spanking
f:stockings
f:x-ray
25 pages
Doujinshi
inits~ehgt.org~t/6d/67/6d67d71c31b43ab086620db367cc7e6a2e741763-3255325-2120-3000-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! | 코스섹스! [Korean]
2018-08-14 11:38
T
korean
translated
original
f:big breasts
f:cosplaying
f:drunk
f:nakadashi
f:stockings
f:unusual pupils
f:x-ray
m:bbm
m:glasses
24 pages
Doujinshi
inits~ehgt.org~t/5c/a2/5ca2eb6a63a0d0600ca07c16cad3fffbf19e8f1e-4959651-2120-3000-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako!
2018-08-13 02:31
T
original
f:big breasts
f:drunk
f:stockings
m:bbm
m:glasses
m:sole male
mugenkidou a
tomose shunsaku
ffm threesome
group
24 pages
Doujinshi
inits~ehgt.org~t/5c/a2/5ca2eb6a63a0d0600ca07c16cad3fffbf19e8f1e-4959651-2120-3000-jpg_250.jpg~(C94) [Mugenkidou A (Tomose Shunsaku)] CosPako! [Chinese] [空気系☆漢化]
2018-08-13 00:03
T
chinese
translated
original
f:big breasts
f:cosplaying
f:drunk
f:spanking
f:stockings
m:bbm
m:glasses
m:sole male
mugenkidou a
26 pages
Doujinshi
inits~ehgt.org~t/ec/43/ec43a65b7b97fac7832e5b807542b2714dbb2cbb-1021450-1280-1814-jpg_250.jpg~(C92) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 8 (Dragon Quest XI) [Spanish] [Lanerte]
2018-08-08 01:40
T
spanish
translated
dragon quest xi
luminary
martina
serena
veronica
f:big breasts
f:bunny girl
f:nakadashi
f:ponytail
m:monster
16 pages
Non-H
inits~ehgt.org~t/2f/59/2f5937db4b9628f749d4d571e03eb94ad95299d6-6034590-5129-6876-jpg_250.jpg~Classroom of the Elite Shunsaku Tomose Art Works
2018-07-08 16:09
T
youkoso jitsuryoku shijou shugi no kyoushitsu e
f:swimsuit
tomose shunsaku
artbook
155 pages
Game CG
inits~ehgt.org~t/c6/30/c630da4cb7410385aa96b75b84e5ed3c4ba9a166-59911-320-450-jpg_250.jpg~[Alice Soft] Double Sensei Life
2018-06-22 21:19
T
f:anal
f:big breasts
f:bikini
f:blowjob
f:defloration
f:footjob
f:glasses
f:kimono
f:maid
f:pantyhose
f:swimsuit
f:teacher
595 pages
Doujinshi
inits~ehgt.org~t/9b/88/9b88f3c6fd6e8d20245d4583a9952aa4c10e7f4c-1165229-2140-3030-jpg_250.jpg~(CSP6) [MUGENKIDOU A (Tomose Shunsaku)] MUGENKIDOU BON Vol. 7 (Kantai Collection -KanColle-)
2018-06-02 00:03
T
kantai collection
haruna
kongou
mutsu
nagato
teitoku
mugenkidou a
tomose shunsaku
full color
mosaic censorship
5 pages
Manga
inits~ehgt.org~t/b1/dd/b1ddb487dbd12851f00f80bb0d91ae6a6344f87d-1074529-1360-1920-jpg_250.jpg~2D Dream Magazine 2018-06 Vol. 100 [Digital]
2018-04-25 03:51
T
f:big breasts
f:double penetration
f:futanari
f:nun
f:schoolgirl uniform
f:stockings
f:tentacles
higuchi isami
parfait
rindou
rusty soul
tomose shunsaku
380 pages
Doujinshi
inits~ehgt.org~t/ae/8b/ae8b371639295dcbbabfe850c027224a525beae3-939648-1000-1417-jpg_250.jpg~(C92) [Mugenkidou A (Tomose Shunsaku)] Mugenkidou Bon! Vol. 8 (Dragon Quest XI) [Vietnamese Tiếng Việt]
2018-02-14 01:56
T
translated
vietnamese
dragon quest xi
luminary
martina
serena
f:big breasts
f:bunny girl
f:ponytail
mugenkidou a
tomose shunsaku
full color
17 pages
<1234567...9>

> Visit the E-Hentai Forums> E-Hentai @ Twitter> Play the HentaiVerse Minigame> Lo-Fi Version

Please read the Terms of Service before participating with or uploading any content to this site.
+ + diff --git a/tests/resources/pages/gelbooru.com/pack-loader-1.html b/tests/resources/pages/gelbooru.com/pack-loader-1.html new file mode 100644 index 000000000..1291d4454 --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-1.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
1 234567»

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-2.html b/tests/resources/pages/gelbooru.com/pack-loader-2.html new file mode 100644 index 000000000..26e252551 --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-2.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
1 2 34567

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-3.html b/tests/resources/pages/gelbooru.com/pack-loader-3.html new file mode 100644 index 000000000..5891418ab --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-3.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
12 3 4567

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-4.html b/tests/resources/pages/gelbooru.com/pack-loader-4.html new file mode 100644 index 000000000..de32a9ff6 --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-4.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
123 4 567

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-5.html b/tests/resources/pages/gelbooru.com/pack-loader-5.html new file mode 100644 index 000000000..ec257ddfa --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-5.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
1234 5 67

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-6.html b/tests/resources/pages/gelbooru.com/pack-loader-6.html new file mode 100644 index 000000000..84bf60c9a --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-6.html @@ -0,0 +1,196 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
12345 6 7

\ No newline at end of file diff --git a/tests/resources/pages/gelbooru.com/pack-loader-7.html b/tests/resources/pages/gelbooru.com/pack-loader-7.html new file mode 100644 index 000000000..a200f4ebe --- /dev/null +++ b/tests/resources/pages/gelbooru.com/pack-loader-7.html @@ -0,0 +1,148 @@ + + + + | Gelbooru + + + + + + + + + + + + + +
+ +
+
+
+ Notice: We are now selling NEW Gelbooru Merch~! Domestic shipping is free on all orders! Do you have an artist tag on Gelbooru? Let us know so we can properly credit you! +
+
+
+
image_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumbimage_thumb

+
«123456 7

\ No newline at end of file diff --git a/tests/src/auth/auth-field-test.cpp b/tests/src/auth/auth-field-test.cpp new file mode 100644 index 000000000..885918c66 --- /dev/null +++ b/tests/src/auth/auth-field-test.cpp @@ -0,0 +1,58 @@ +#include "auth-field-test.h" +#include +#include +#include "auth/auth-const-field.h" +#include "auth/auth-field.h" +#include "auth/auth-hash-field.h" +#include "mixed-settings.h" + + +MixedSettings *makeSettings(QString key, QString value) +{ + QSettings *settings = new QSettings("tests/resources/settings.ini", QSettings::IniFormat); + settings->setValue(key, value); + + return new MixedSettings(QList() << settings); +} + + +void AuthFieldTest::testBasic() +{ + AuthField field("id", "key", AuthField::FieldType::Text); + + QCOMPARE(field.id(), QString("id")); + QCOMPARE(field.key(), QString("key")); + QCOMPARE(field.type(), AuthField::FieldType::Text); + + MixedSettings *settings = makeSettings("auth/id", "user"); + QCOMPARE(field.value(settings), QString("user")); + settings->deleteLater(); +} + +void AuthFieldTest::testConst() +{ + AuthConstField field("key", "val"); + + QCOMPARE(field.key(), QString("key")); + QCOMPARE(field.type(), AuthField::FieldType::Const); + + MixedSettings *settings = new MixedSettings(QList()); + QCOMPARE(field.value(settings), QString("val")); + settings->deleteLater(); +} + +void AuthFieldTest::testHash() +{ + AuthHashField field("key", QCryptographicHash::Algorithm::Md5, "test-%pseudo%"); + + QCOMPARE(field.key(), QString("key")); + QCOMPARE(field.type(), AuthField::FieldType::Hash); + QCOMPARE(field.salt(), QString("test-%pseudo%")); + + MixedSettings *settings = makeSettings("auth/pseudo", "user"); + QCOMPARE(field.value(settings), QString("42b27efc1480b4fe6d7eaa5eec47424d")); // md5("test-user") + settings->deleteLater(); +} + + +QTEST_MAIN(AuthFieldTest) diff --git a/tests/src/auth/auth-field-test.h b/tests/src/auth/auth-field-test.h new file mode 100644 index 000000000..94d33b8c0 --- /dev/null +++ b/tests/src/auth/auth-field-test.h @@ -0,0 +1,17 @@ +#ifndef AUTH_FIELD_TEST_H +#define AUTH_FIELD_TEST_H + +#include "test-suite.h" + + +class AuthFieldTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testBasic(); + void testConst(); + void testHash(); +}; + +#endif // AUTH_FIELD_TEST_H diff --git a/tests/src/auth/auth-test.cpp b/tests/src/auth/auth-test.cpp new file mode 100644 index 000000000..c0227f7ab --- /dev/null +++ b/tests/src/auth/auth-test.cpp @@ -0,0 +1,39 @@ +#include "auth-test.h" +#include +#include "auth/http-auth.h" +#include "auth/oauth2-auth.h" +#include "auth/url-auth.h" + + +void AuthTest::testUrlAuth() +{ + QList fields; + UrlAuth auth("url", fields, 50); + + QCOMPARE(auth.type(), QString("url")); + QCOMPARE(auth.fields(), fields); + QCOMPARE(auth.maxPage(), 50); +} + +void AuthTest::testHttpAuth() +{ + QList fields; + HttpAuth auth("post", "https://www.google.com", fields, "cookie"); + + QCOMPARE(auth.type(), QString("post")); + QCOMPARE(auth.url(), QString("https://www.google.com")); + QCOMPARE(auth.fields(), fields); + QCOMPARE(auth.cookie(), QString("cookie")); +} + +void AuthTest::testOAuth2Auth() +{ + OAuth2Auth auth("oauth2", "password", "https://www.google.com"); + + QCOMPARE(auth.type(), QString("oauth2")); + QCOMPARE(auth.authType(), QString("password")); + QCOMPARE(auth.tokenUrl(), QString("https://www.google.com")); +} + + +QTEST_MAIN(AuthTest) diff --git a/tests/src/auth/auth-test.h b/tests/src/auth/auth-test.h new file mode 100644 index 000000000..d9be79d8f --- /dev/null +++ b/tests/src/auth/auth-test.h @@ -0,0 +1,17 @@ +#ifndef AUTH_TEST_H +#define AUTH_TEST_H + +#include "test-suite.h" + + +class AuthTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testUrlAuth(); + void testHttpAuth(); + void testOAuth2Auth(); +}; + +#endif // AUTH_TEST_H diff --git a/tests/src/commands/sql-worker-test.cpp b/tests/src/commands/sql-worker-test.cpp index a26e26be9..636130bfd 100644 --- a/tests/src/commands/sql-worker-test.cpp +++ b/tests/src/commands/sql-worker-test.cpp @@ -70,8 +70,9 @@ void SqlWorkerTest::testExecCreateAndInsert() QSqlQuery query = db.exec("SELECT some_value FROM test_table"); int idVal = query.record().indexOf("some_value"); QList values; - while (query.next()) + while (query.next()) { values.append(query.value(idVal).toInt()); + } QCOMPARE(values, QList() << 1 << 3 << 21); } diff --git a/tests/src/downloader/download-query-group-test.cpp b/tests/src/downloader/download-query-group-test.cpp new file mode 100644 index 000000000..c463262c8 --- /dev/null +++ b/tests/src/downloader/download-query-group-test.cpp @@ -0,0 +1,57 @@ +#include "download-query-group-test.h" +#include +#include "downloader/download-query-group.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void DownloadQueryGroupTest::testCompare() +{ + DownloadQueryGroup a(QStringList() << "tags", 1, 2, 3, QStringList() << "postFiltering", true, nullptr, "filename", "path"); + DownloadQueryGroup b(QStringList() << "tags", 1, 2, 3, QStringList() << "postFiltering", true, nullptr, "filename", "path"); + DownloadQueryGroup c(QStringList() << "tags", 1, 3, 3, QStringList() << "postFiltering", true, nullptr, "filename", "path"); + DownloadQueryGroup d(QStringList() << "tags", 1, 3, 3, QStringList() << "postFiltering", true, nullptr, "filename", "path"); + + d.progressVal = 37; + d.progressFinished = false; + + QVERIFY(a == b); + QVERIFY(b == a); + QVERIFY(a != c); + QVERIFY(b != c); + QVERIFY(c == c); + QVERIFY(c == d); // The progress status must NOT be checked +} + +void DownloadQueryGroupTest::testSerialization() +{ + Profile profile("tests/resources/"); + Source source(&profile, "tests/resources/sites/Danbooru (2.0)"); + Site site("danbooru.donmai.us", &source); + + DownloadQueryGroup original(QStringList() << "tags", 1, 2, 3, QStringList() << "postFiltering", true, &site, "filename", "path"); + original.progressVal = 37; + original.progressFinished = false; + + QJsonObject json; + original.write(json); + + DownloadQueryGroup dest; + dest.read(json, QMap {{ site.url(), &site }}); + + QCOMPARE(dest.query.tags, QStringList() << "tags"); + QCOMPARE(dest.page, 1); + QCOMPARE(dest.perpage, 2); + QCOMPARE(dest.total, 3); + QCOMPARE(dest.postFiltering, QStringList() << "postFiltering"); + QCOMPARE(dest.getBlacklisted, true); + QCOMPARE(dest.site, &site); + QCOMPARE(dest.filename, QString("filename")); + QCOMPARE(dest.path, QString("path")); + QCOMPARE(dest.progressVal, 37); + QCOMPARE(dest.progressFinished, false); +} + + +QTEST_MAIN(DownloadQueryGroupTest) diff --git a/tests/src/downloader/download-query-group-test.h b/tests/src/downloader/download-query-group-test.h new file mode 100644 index 000000000..f37aefba6 --- /dev/null +++ b/tests/src/downloader/download-query-group-test.h @@ -0,0 +1,16 @@ +#ifndef DOWNLOAD_QUERY_GROUP_TEST_H +#define DOWNLOAD_QUERY_GROUP_TEST_H + +#include "test-suite.h" + + +class DownloadQueryGroupTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testCompare(); + void testSerialization(); +}; + +#endif // DOWNLOAD_QUERY_GROUP_TEST_H diff --git a/tests/src/downloader/download-query-image-test.cpp b/tests/src/downloader/download-query-image-test.cpp new file mode 100644 index 000000000..249ab7384 --- /dev/null +++ b/tests/src/downloader/download-query-image-test.cpp @@ -0,0 +1,70 @@ +#include "download-query-image-test.h" +#include +#include +#include +#include "downloader/download-query-image.h" +#include "models/image.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void DownloadQueryImageTest::testCompare() +{ + Profile profile("tests/resources/"); + Source source(&profile, "tests/resources/sites/Danbooru (2.0)"); + Site site("danbooru.donmai.us", &source); + + auto img1 = QSharedPointer(new Image(&site, {{ "id", "1" }}, &profile)); + auto img2 = QSharedPointer(new Image(&site, {{ "id", "2" }}, &profile)); + + DownloadQueryImage a(img1, &site, "filename", "path"); + DownloadQueryImage b(img1, &site, "filename", "path"); + DownloadQueryImage c(img2, &site, "filename", "path"); + + QVERIFY(a == b); + QVERIFY(b == a); + QVERIFY(a != c); + QVERIFY(b != c); + QVERIFY(c == c); +} + +void DownloadQueryImageTest::testSerialization() +{ + Profile profile("tests/resources/"); + Source source(&profile, "tests/resources/sites/Danbooru (2.0)"); + Site site("danbooru.donmai.us", &source); + + QMap details = { + { "id", "1" }, + { "md5", "md5" }, + { "rating", "rating" }, + { "tags", "tags" }, + { "file_url", "https://test.com/fileUrl" }, + { "date", "2016-08-26T16:26:30+01:00" }, + { "search", "search" }, + }; + auto img = QSharedPointer(new Image(&site, details, &profile)); + DownloadQueryImage original(img, &site, "filename", "path"); + + QJsonObject json; + original.write(json); + + DownloadQueryImage dest; + dest.read(json, QMap {{ site.url(), &site }}); + + QCOMPARE(static_cast(dest.image->id()), 1); + QCOMPARE(dest.image->md5(), QString("md5")); + QCOMPARE(dest.image->rating(), QString("rating")); + QCOMPARE(dest.image->tagsString(), QStringList() << "tags"); + QCOMPARE(dest.image->fileUrl().toString(), QString("https://test.com/fileUrl")); + QCOMPARE(dest.image->createdAt().toString("yyyy-MM-dd HH:mm:ss"), QString("2016-08-26 16:26:30")); + QCOMPARE(dest.image->search(), QStringList() << "search"); + + QCOMPARE(dest.site, &site); + QCOMPARE(dest.filename, QString("filename")); + QCOMPARE(dest.path, QString("path")); +} + + +QTEST_MAIN(DownloadQueryImageTest) diff --git a/tests/src/downloader/download-query-image-test.h b/tests/src/downloader/download-query-image-test.h new file mode 100644 index 000000000..b66586ba1 --- /dev/null +++ b/tests/src/downloader/download-query-image-test.h @@ -0,0 +1,16 @@ +#ifndef DOWNLOAD_QUERY_IMAGE_TEST_H +#define DOWNLOAD_QUERY_IMAGE_TEST_H + +#include "test-suite.h" + + +class DownloadQueryImageTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testCompare(); + void testSerialization(); +}; + +#endif // DOWNLOAD_QUERY_IMAGE_TEST_H diff --git a/tests/src/downloader/file-downloader-test.cpp b/tests/src/downloader/file-downloader-test.cpp index d546226d2..7a7f54541 100644 --- a/tests/src/downloader/file-downloader-test.cpp +++ b/tests/src/downloader/file-downloader-test.cpp @@ -8,8 +8,9 @@ QString fileMd5(const QString &path) QCryptographicHash hash(QCryptographicHash::Md5); QFile f(path); - if (!f.open(QFile::ReadOnly)) + if (!f.open(QFile::ReadOnly)) { return QString(); + } hash.addData(&f); f.close(); @@ -41,8 +42,7 @@ void FileDownloaderTest::testSuccessMultiple() QVERIFY(downloader.start(reply, dest)); QVERIFY(spy.wait()); - for (const QString &path : dest) - { + for (const QString &path : dest) { QCOMPARE(fileMd5(path), m_successMd5); QFile::remove(path); } diff --git a/tests/src/downloader/image-downloader-test.cpp b/tests/src/downloader/image-downloader-test.cpp index 1fe2c15d3..e2da8923e 100644 --- a/tests/src/downloader/image-downloader-test.cpp +++ b/tests/src/downloader/image-downloader-test.cpp @@ -2,6 +2,7 @@ #include #include "custom-network-access-manager.h" #include "downloader/image-downloader.h" +#include "models/filtering/blacklist.h" #include "models/image.h" #include "models/profile.h" #include "models/site.h" @@ -16,8 +17,7 @@ void ImageDownloaderTest::initTestCase() void ImageDownloaderTest::cleanup() { QDir dir("tests/resources/tmp/"); - for (const QString &file : dir.entryList(QDir::Files)) - { + for (const QString &file : dir.entryList(QDir::Files)) { dir.remove(file); } @@ -31,8 +31,9 @@ void ImageDownloaderTest::cleanup() Image *ImageDownloaderTest::createImage(bool noMd5) { QMap details; - if (!noMd5) - { details["md5"] = "1bc29b36f623ba82aaf6724fd3b16718"; } + if (!noMd5) { + details["md5"] = "1bc29b36f623ba82aaf6724fd3b16718"; + } details["ext"] = "jpg"; details["id"] = "7331"; details["file_url"] = "http://test.com/img/oldfilename.jpg"; @@ -41,8 +42,9 @@ Image *ImageDownloaderTest::createImage(bool noMd5) details["page_url"] = "/posts/7331"; details["tags"] = "tag1 tag2 tag3"; - if (m_profile == nullptr) - { m_profile = new Profile("tests/resources/"); } + if (m_profile == nullptr) { + m_profile = new Profile("tests/resources/"); + } m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); m_site = new Site("danbooru.donmai.us", m_source); @@ -53,10 +55,10 @@ Image *ImageDownloaderTest::createImage(bool noMd5) void ImageDownloaderTest::testSuccessBasic() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::Size::Full, Image::SaveResult::Saved }); assertDownload(img, &downloader, expected, true); } @@ -64,10 +66,10 @@ void ImageDownloaderTest::testSuccessBasic() void ImageDownloaderTest::testSuccessLoadTags() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "%copyright%.%ext%", "tests/resources/tmp", 1, false, false, true, nullptr, true, false); + ImageDownloader downloader(m_profile, img, "%copyright%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, true, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/to heart 2.jpg"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/to heart 2.jpg"), Image::Size::Full, Image::SaveResult::Saved }); assertDownload(img, &downloader, expected, true); } @@ -75,20 +77,21 @@ void ImageDownloaderTest::testSuccessLoadTags() void ImageDownloaderTest::testSuccessLoadTagsExternal() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, true, nullptr, true, false); + ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, nullptr, true, false); // Delete already existing QFile logFile("tests/resources/tmp/savelog.txt"); - if (logFile.exists()) + if (logFile.exists()) { logFile.remove(); + } QSettings *settings = m_profile->getSettings(); settings->setValue("LogFiles/0/locationType", 1); settings->setValue("LogFiles/0/uniquePath", logFile.fileName()); settings->setValue("LogFiles/0/content", "%copyright%"); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::Size::Full, Image::SaveResult::Saved }); assertDownload(img, &downloader, expected, true); @@ -104,13 +107,26 @@ void ImageDownloaderTest::testSuccessLoadTagsExternal() settings->remove("LogFiles/0/content"); } +void ImageDownloaderTest::testSuccessLoadSize() +{ + QSharedPointer img(createImage()); + ImageDownloader downloader(m_profile, img, "%copyright%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, true, false); + + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/to heart 2.jpg"), Image::Size::Full, Image::SaveResult::Saved }); + + QVERIFY(img->size().isEmpty()); + assertDownload(img, &downloader, expected, true); + QCOMPARE(img->size(), QSize(1, 1)); +} + void ImageDownloaderTest::testOpenError() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "///", "///root/toto", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "///", "///root/toto", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("//root/toto/"), Image::SaveResult::Error); + QList expected; + expected.append({ QDir::toNativeSeparators("//root/toto/"), Image::Size::Full, Image::SaveResult::Error }); assertDownload(img, &downloader, expected, false, true); } @@ -118,10 +134,10 @@ void ImageDownloaderTest::testOpenError() void ImageDownloaderTest::testNotFound() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::SaveResult::NotFound); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::Size::Full, Image::SaveResult::NotFound }); CustomNetworkAccessManager::NextFiles.append("404"); @@ -131,10 +147,10 @@ void ImageDownloaderTest::testNotFound() void ImageDownloaderTest::testNetworkError() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::SaveResult::NetworkError); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::Size::Full, Image::SaveResult::NetworkError }); CustomNetworkAccessManager::NextFiles.append("500"); @@ -144,10 +160,10 @@ void ImageDownloaderTest::testNetworkError() void ImageDownloaderTest::testOriginalMd5() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/1bc29b36f623ba82aaf6724fd3b16718.jpg"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/1bc29b36f623ba82aaf6724fd3b16718.jpg"), Image::Size::Full, Image::SaveResult::Saved }); assertDownload(img, &downloader, expected, true); } @@ -155,10 +171,10 @@ void ImageDownloaderTest::testOriginalMd5() void ImageDownloaderTest::testGeneratedMd5() { QSharedPointer img(createImage(true)); - ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, true, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, false, false); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/956ddde86fb5ce85218b21e2f49e5c50.jpg"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/956ddde86fb5ce85218b21e2f49e5c50.jpg"), Image::Size::Full, Image::SaveResult::Saved }); assertDownload(img, &downloader, expected, true); } @@ -166,26 +182,39 @@ void ImageDownloaderTest::testGeneratedMd5() void ImageDownloaderTest::testRotateExtension() { QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, true, nullptr, false, true); + ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, false, true); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/1bc29b36f623ba82aaf6724fd3b16718.png"), Image::SaveResult::Saved); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/1bc29b36f623ba82aaf6724fd3b16718.png"), Image::Size::Full, Image::SaveResult::Saved }); CustomNetworkAccessManager::NextFiles.append("404"); assertDownload(img, &downloader, expected, true); } +void ImageDownloaderTest::testSampleFallback() +{ + QSharedPointer img(createImage()); + ImageDownloader downloader(m_profile, img, "%md5%.%ext%", "tests/resources/tmp", 1, false, false, nullptr, false, false); + + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/1bc29b36f623ba82aaf6724fd3b16718.jpg"), Image::Size::Sample, Image::SaveResult::Saved }); + + CustomNetworkAccessManager::NextFiles.append("404"); + + assertDownload(img, &downloader, expected, true, false, true); +} + void ImageDownloaderTest::testBlacklisted() { - m_profile = new Profile("tests/resources/"); - m_profile->addBlacklistedTag("tag1"); + Blacklist blacklist(QStringList() << "tag1"); QSharedPointer img(createImage()); - ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, false, nullptr, false, false); + ImageDownloader downloader(m_profile, img, "out.jpg", "tests/resources/tmp", 1, false, false, nullptr, false, false); + downloader.setBlacklist(&blacklist); - QMap expected; - expected.insert(QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::SaveResult::Blacklisted); + QList expected; + expected.append({ QDir::toNativeSeparators("tests/resources/tmp/out.jpg"), Image::Size::Full, Image::SaveResult::Blacklisted }); assertDownload(img, &downloader, expected, false); @@ -193,32 +222,39 @@ void ImageDownloaderTest::testBlacklisted() } -void ImageDownloaderTest::assertDownload(QSharedPointer img, ImageDownloader *downloader, const QMap &expected, bool shouldExist, bool onlyCheckValues) +void ImageDownloaderTest::assertDownload(QSharedPointer img, ImageDownloader *downloader, const QList &expected, bool shouldExist, bool onlyCheckValues, bool sampleFallback) { - qRegisterMetaType>(); - QSignalSpy spy(downloader, SIGNAL(saved(QSharedPointer, QMap))); + const bool oldSampleFallback = m_profile->getSettings()->value("Save/samplefallback", true).toBool(); + m_profile->getSettings()->setValue("Save/samplefallback", sampleFallback); + + qRegisterMetaType>(); + QSignalSpy spy(downloader, SIGNAL(saved(QSharedPointer, QList))); QTimer::singleShot(1, downloader, SLOT(save())); QVERIFY(spy.wait()); QList arguments = spy.takeFirst(); auto out = arguments[0].value>(); - auto result = arguments[1].value>(); + auto result = arguments[1].value>(); + + m_profile->getSettings()->setValue("Save/samplefallback", oldSampleFallback); QCOMPARE(out, img); - qDebug() << "result" << result; - qDebug() << "expected" << expected; - if (onlyCheckValues) - { QCOMPARE(result.values(), expected.values()); } - else - { QCOMPARE(result, expected); } - - for (const QString &path : result.keys()) - { - QFile f(path); + QCOMPARE(result.count(), expected.count()); + for (int i = 0; i < result.count(); ++i) { + if (!onlyCheckValues) { + QCOMPARE(result[i].path, expected[i].path); + } + QCOMPARE(result[i].size, expected[i].size); + QCOMPARE(result[i].result, expected[i].result); + } + + for (const ImageSaveResult &res : result) { + QFile f(res.path); bool exists = f.exists(); QVERIFY(exists == shouldExist); - if (exists) - { f.remove(); } + if (exists) { + f.remove(); + } } } diff --git a/tests/src/downloader/image-downloader-test.h b/tests/src/downloader/image-downloader-test.h index 854998274..8bb08d2f3 100644 --- a/tests/src/downloader/image-downloader-test.h +++ b/tests/src/downloader/image-downloader-test.h @@ -1,7 +1,7 @@ #ifndef IMAGE_DOWNLOADER_TEST_H #define IMAGE_DOWNLOADER_TEST_H -#include +#include #include #include #include "models/image.h" @@ -9,6 +9,7 @@ class ImageDownloader; +struct ImageSaveResult; class Profile; class Site; class Source; @@ -24,17 +25,19 @@ class ImageDownloaderTest : public TestSuite void testSuccessBasic(); void testSuccessLoadTags(); void testSuccessLoadTagsExternal(); + void testSuccessLoadSize(); void testOpenError(); void testNotFound(); void testNetworkError(); void testOriginalMd5(); void testGeneratedMd5(); void testRotateExtension(); + void testSampleFallback(); void testBlacklisted(); protected: Image *createImage(bool noMd5 = false); - void assertDownload(QSharedPointer img, ImageDownloader *downloader, const QMap &expected, bool shouldExist, bool onlyCheckValues = false); + void assertDownload(QSharedPointer img, ImageDownloader *downloader, const QList &expected, bool shouldExist, bool onlyCheckValues = false, bool sampleFallback = false); private: Profile *m_profile; diff --git a/tests/src/downloader/image-save-result-test.cpp b/tests/src/downloader/image-save-result-test.cpp new file mode 100644 index 000000000..1a637dea1 --- /dev/null +++ b/tests/src/downloader/image-save-result-test.cpp @@ -0,0 +1,31 @@ +#include "image-save-result-test.h" +#include +#include "downloader/image-save-result.h" + + +void ImageSaveResultTest::testCompare() +{ + ImageSaveResult a; + a.path = "path"; + a.size = Image::Size::Full; + a.result = Image::SaveResult::Saved; + + ImageSaveResult b; + b.path = "path"; + b.size = Image::Size::Full; + b.result = Image::SaveResult::Saved; + + ImageSaveResult c; + c.path = "sample"; + c.size = Image::Size::Sample; + c.result = Image::SaveResult::Saved; + + QVERIFY(a == b); + QVERIFY(b == a); + QVERIFY(a != c); + QVERIFY(b != c); + QVERIFY(c == c); +} + + +QTEST_MAIN(ImageSaveResultTest) diff --git a/tests/src/downloader/image-save-result-test.h b/tests/src/downloader/image-save-result-test.h new file mode 100644 index 000000000..d2ebbb475 --- /dev/null +++ b/tests/src/downloader/image-save-result-test.h @@ -0,0 +1,15 @@ +#ifndef IMAGE_SAVE_RESULT_TEST_H +#define IMAGE_SAVE_RESULT_TEST_H + +#include "test-suite.h" + + +class ImageSaveResultTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testCompare(); +}; + +#endif // IMAGE_SAVE_RESULT_TEST_H diff --git a/tests/src/exponential-moving-average-test.cpp b/tests/src/exponential-moving-average-test.cpp new file mode 100644 index 000000000..05110f61c --- /dev/null +++ b/tests/src/exponential-moving-average-test.cpp @@ -0,0 +1,52 @@ +#include "exponential-moving-average-test.h" +#include +#include "exponential-moving-average.h" + + +void ExponentialMovingAverageTest::testEmpty() +{ + ExponentialMovingAverage avg(0.5); + + QCOMPARE(avg.average(), 0.0); +} + +void ExponentialMovingAverageTest::testClear() +{ + ExponentialMovingAverage avg(0.5); + avg.addValue(1); + avg.clear(); + + QCOMPARE(avg.average(), 0.0); +} + +void ExponentialMovingAverageTest::testFirstValue() +{ + ExponentialMovingAverage avg(0.5); + avg.addValue(1); + + QCOMPARE(avg.average(), 1.0); +} + +void ExponentialMovingAverageTest::testBasic() +{ + ExponentialMovingAverage avg(0.5); + avg.addValue(2); + avg.addValue(4); + avg.addValue(5); + + QCOMPARE(avg.average(), 4.0); +} + +void ExponentialMovingAverageTest::testSetSmoothingFactor() +{ + ExponentialMovingAverage avg(1); + avg.setSmoothingFactor(0.5); + avg.addValue(2); + avg.addValue(4); + avg.addValue(5); + + QCOMPARE(avg.average(), 4.0); +} + + +QTEST_MAIN(ExponentialMovingAverageTest) diff --git a/tests/src/exponential-moving-average-test.h b/tests/src/exponential-moving-average-test.h new file mode 100644 index 000000000..8f38f957c --- /dev/null +++ b/tests/src/exponential-moving-average-test.h @@ -0,0 +1,19 @@ +#ifndef EXPONENTIAL_MOVING_AVERAGE_TEST_H +#define EXPONENTIAL_MOVING_AVERAGE_TEST_H + +#include "test-suite.h" + + +class ExponentialMovingAverageTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testEmpty(); + void testClear(); + void testFirstValue(); + void testBasic(); + void testSetSmoothingFactor(); +}; + +#endif // EXPONENTIAL_MOVING_AVERAGE_TEST_H diff --git a/tests/src/filename/filename-condition-visitor-test.cpp b/tests/src/filename/filename-condition-visitor-test.cpp new file mode 100644 index 000000000..6d56949ed --- /dev/null +++ b/tests/src/filename/filename-condition-visitor-test.cpp @@ -0,0 +1,163 @@ +#include "filename-condition-visitor-test.h" +#include +#include +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-node-condition-javascript.h" +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/filename-condition-visitor.h" +#include "loader/token.h" + + +using NInvert = FilenameNodeConditionInvert; +using NOp = FilenameNodeConditionOp; +using NTag = FilenameNodeConditionTag; +using NToken = FilenameNodeConditionToken; + +void FilenameConditionVisitorTest::testTag() +{ + NTag condition(Tag("my_tag")); + + QMap tokensWithTag = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "my_tag" << "tag3") }, + }; + QMap tokensWithoutTag = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "tag3") }, + }; + QMap tokensWithoutAnyTag; + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokensWithTag, &settings).run(condition), true); + QCOMPARE(FilenameConditionVisitor(tokensWithoutTag, &settings).run(condition), false); + QCOMPARE(FilenameConditionVisitor(tokensWithoutAnyTag, &settings).run(condition), false); +} + +void FilenameConditionVisitorTest::testToken() +{ + NToken condition("my_token"); + + QMap tokensWithToken = { + { "my_token", Token("not_empty") }, + }; + QMap tokensWithEmptyToken = { + { "my_token", Token("") }, + }; + QMap tokensWithoutToken; + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokensWithToken, &settings).run(condition), true); + QCOMPARE(FilenameConditionVisitor(tokensWithEmptyToken, &settings).run(condition), false); + QCOMPARE(FilenameConditionVisitor(tokensWithoutToken, &settings).run(condition), false); +} + +void FilenameConditionVisitorTest::testOperatorOr() +{ + QMap tokens = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "tag3") }, + }; + + auto op = NOp::Operator::Or; + NOp trueTrue(op, new NTag(Tag("tag1")), new NTag(Tag("tag1"))); + NOp trueFalse(op, new NTag(Tag("tag1")), new NTag(Tag("not_found"))); + NOp falseTrue(op, new NTag(Tag("not_found")), new NTag(Tag("tag1"))); + NOp falseFalse(op, new NTag(Tag("not_found")), new NTag(Tag("not_found"))); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(trueTrue), true); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(trueFalse), true); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(falseTrue), true); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(falseFalse), false); +} + +void FilenameConditionVisitorTest::testOperatorAnd() +{ + QMap tokens = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "tag3") }, + }; + + auto op = NOp::Operator::And; + NOp trueTrue(op, new NTag(Tag("tag1")), new NTag(Tag("tag1"))); + NOp trueFalse(op, new NTag(Tag("tag1")), new NTag(Tag("not_found"))); + NOp falseTrue(op, new NTag(Tag("not_found")), new NTag(Tag("tag1"))); + NOp falseFalse(op, new NTag(Tag("not_found")), new NTag(Tag("not_found"))); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(trueTrue), true); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(trueFalse), false); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(falseTrue), false); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(falseFalse), false); +} + +void FilenameConditionVisitorTest::testMixedOperators() +{ + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + + QMap tokens = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "tag3") }, + }; + + // A || (!B && C), A = true, B = false, C = false => true + NOp left( + NOp::Operator::Or, + new NTag(Tag("tag1")), + new NOp( + NOp::Operator::And, + new NInvert(new NTag(Tag("not_found"))), + new NTag(Tag("not_found")) + ) + ); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(left), true); + + // (A || !B) && C, A = true, B = false, C = false => false + NOp right( + NOp::Operator::And, + new NOp( + NOp::Operator::Or, + new NTag(Tag("tag1")), + new NInvert(new NTag(Tag("not_found"))) + ), + new NTag(Tag("not_found")) + ); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(right), false); +} + +void FilenameConditionVisitorTest::testInvert() +{ + QMap tokens = { + { "allos", Token(QStringList() << "tag1" << "tag2" << "tag3") }, + }; + + auto validTag = new NTag(Tag("tag1")); + auto invalidToken = new NToken("not_found"); + + auto validOp = new NOp(NOp::Operator::Or, new NTag(Tag("tag1")), new NToken("not_found")); + auto invalidOp = new NOp(NOp::Operator::And, new NTag(Tag("tag1")), new NToken("not_found")); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(NInvert(validTag)), false); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(NInvert(invalidToken)), true); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(NInvert(validOp)), false); + QCOMPARE(FilenameConditionVisitor(tokens, &settings).run(NInvert(invalidOp)), true); +} + +void FilenameConditionVisitorTest::testJavaScript() +{ + FilenameNodeConditionJavaScript condition("typeof my_token !== 'undefined' && my_token.length > 0"); + + QMap tokensWithToken = { + { "my_token", Token("not_empty") }, + }; + QMap tokensWithEmptyToken = { + { "my_token", Token("") }, + }; + QMap tokensWithoutToken; + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + QCOMPARE(FilenameConditionVisitor(tokensWithToken, &settings).run(condition), true); + QCOMPARE(FilenameConditionVisitor(tokensWithEmptyToken, &settings).run(condition), false); + QCOMPARE(FilenameConditionVisitor(tokensWithoutToken, &settings).run(condition), false); +} + + +QTEST_MAIN(FilenameConditionVisitorTest) diff --git a/tests/src/filename/filename-condition-visitor-test.h b/tests/src/filename/filename-condition-visitor-test.h new file mode 100644 index 000000000..b5bfea7f6 --- /dev/null +++ b/tests/src/filename/filename-condition-visitor-test.h @@ -0,0 +1,21 @@ +#ifndef FILENAME_CONDITION_VISITOR_TEST_H +#define FILENAME_CONDITION_VISITOR_TEST_H + +#include "test-suite.h" + + +class FilenameConditionVisitorTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testTag(); + void testToken(); + void testOperatorOr(); + void testOperatorAnd(); + void testMixedOperators(); + void testInvert(); + void testJavaScript(); +}; + +#endif // FILENAME_CONDITION_VISITOR_TEST_H diff --git a/tests/src/filename/filename-execution-visitor-test.cpp b/tests/src/filename/filename-execution-visitor-test.cpp new file mode 100644 index 000000000..213a32fdc --- /dev/null +++ b/tests/src/filename/filename-execution-visitor-test.cpp @@ -0,0 +1,62 @@ +#include "filename-execution-visitor-test.h" +#include +#include +#include "filename/filename-execution-visitor.h" +#include "filename/filename-parser.h" +#include "loader/token.h" +#include "models/profile.h" + + +void FilenameExecutionVisitorTest::testEmpty() +{ + QMap tokens { + { "md5", Token("1bc29b36f623ba82aaf6724fd3b16718") }, + { "ext", Token("jpg") } + }; + + FilenameParser parser(""); + auto ast = parser.parseRoot(); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + FilenameExecutionVisitor executionVisitor(tokens, &settings); + QString result = executionVisitor.run(*ast); + + QCOMPARE(result, QString()); +} + +void FilenameExecutionVisitorTest::testBasic() +{ + QMap tokens { + { "md5", Token("1bc29b36f623ba82aaf6724fd3b16718") }, + { "ext", Token("jpg") } + }; + + FilenameParser parser("image.jpg"); + auto ast = parser.parseRoot(); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + FilenameExecutionVisitor executionVisitor(tokens, &settings); + QString result = executionVisitor.run(*ast); + + QCOMPARE(result, QString("image.jpg")); +} + +void FilenameExecutionVisitorTest::testToken() +{ + QMap tokens { + { "md5", Token("1bc29b36f623ba82aaf6724fd3b16718") }, + { "ext", Token("jpg") } + }; + + FilenameParser parser("out/%md5%.%ext%"); + auto ast = parser.parseRoot(); + + QSettings settings("tests/resources/settings.ini", QSettings::IniFormat); + FilenameExecutionVisitor executionVisitor(tokens, &settings); + QString result = executionVisitor.run(*ast); + + QCOMPARE(result, QString("out/1bc29b36f623ba82aaf6724fd3b16718.jpg")); +} + + +QTEST_MAIN(FilenameExecutionVisitorTest) diff --git a/tests/src/filename/filename-execution-visitor-test.h b/tests/src/filename/filename-execution-visitor-test.h new file mode 100644 index 000000000..07b577dac --- /dev/null +++ b/tests/src/filename/filename-execution-visitor-test.h @@ -0,0 +1,17 @@ +#ifndef FILENAME_EXECUTION_VISITOR_TEST_H +#define FILENAME_EXECUTION_VISITOR_TEST_H + +#include "test-suite.h" + + +class FilenameExecutionVisitorTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testEmpty(); + void testBasic(); + void testToken(); +}; + +#endif // FILENAME_EXECUTION_VISITOR_TEST_H diff --git a/tests/src/filename/filename-parser-test.cpp b/tests/src/filename/filename-parser-test.cpp new file mode 100644 index 000000000..7669de7b1 --- /dev/null +++ b/tests/src/filename/filename-parser-test.cpp @@ -0,0 +1,318 @@ +#include "filename-parser-test.h" +#include +#include +#include +#include "filename/ast/filename-node-condition-invert.h" +#include "filename/ast/filename-node-condition-op.h" +#include "filename/ast/filename-node-condition-tag.h" +#include "filename/ast/filename-node-condition-token.h" +#include "filename/ast/filename-node-conditional.h" +#include "filename/ast/filename-node-root.h" +#include "filename/ast/filename-node-text.h" +#include "filename/ast/filename-node-variable.h" +#include "filename/filename-parser.h" + + +void FilenameParserTest::testParseEmpty() +{ + FilenameParser parser(""); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QVERIFY(filename->exprs.isEmpty()); +} + +void FilenameParserTest::testParseText() +{ + FilenameParser parser("image.png"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 1); + + auto txt = dynamic_cast(filename->exprs[0]); + QVERIFY(txt != nullptr); + QCOMPARE(txt->text, QString("image.png")); +} + +void FilenameParserTest::testParseVariable() +{ + FilenameParser parser("%md5%"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 1); + + auto var = dynamic_cast(filename->exprs[0]); + QVERIFY(var != nullptr); + QCOMPARE(var->name, QString("md5")); + QCOMPARE(var->opts.count(), 0); +} + +void FilenameParserTest::testParseVariableWithOptions() +{ + FilenameParser parser("%md5:flag,opt=val%"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 1); + + auto var = dynamic_cast(filename->exprs[0]); + QVERIFY(var != nullptr); + QCOMPARE(var->name, QString("md5")); + QCOMPARE(var->opts.count(), 2); + QCOMPARE(var->opts.keys(), QList() << "flag" << "opt"); + QCOMPARE(var->opts["flag"], QString()); + QCOMPARE(var->opts["opt"], QString("val")); +} + +void FilenameParserTest::testParseMixed() +{ + FilenameParser parser("out/%md5%.%ext%"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 4); +} + + +void FilenameParserTest::testParseConditional() +{ + FilenameParser parser("out/<\"tag\"?some tag is present:%artist%>/image.png"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 3); + + auto txt1 = dynamic_cast(filename->exprs[0]); + QVERIFY(txt1 != nullptr); + QCOMPARE(txt1->text, QString("out/")); + + auto conditional = dynamic_cast(filename->exprs[1]); + QVERIFY(conditional != nullptr); + QVERIFY(conditional->ifTrue != nullptr); + QVERIFY(conditional->ifFalse != nullptr); + + auto cond = dynamic_cast(conditional->condition); + QVERIFY(cond != nullptr); + QCOMPARE(cond->tag.text(), QString("tag")); + + auto ifTrue = dynamic_cast(conditional->ifTrue); + QVERIFY(ifTrue != nullptr); + QCOMPARE(ifTrue->text, QString("some tag is present")); + + auto ifFalse = dynamic_cast(conditional->ifFalse); + QVERIFY(ifFalse != nullptr); + QCOMPARE(ifFalse->name, QString("artist")); + + auto txt2 = dynamic_cast(filename->exprs[2]); + QVERIFY(txt2 != nullptr); + QCOMPARE(txt2->text, QString("/image.png")); +} + +void FilenameParserTest::testParseConditionalLegacy() +{ + FilenameParser parser("out/image.png"); + auto filename = parser.parseRoot(); + QVERIFY(parser.error().isEmpty()); + + QCOMPARE(filename->exprs.count(), 3); + + auto txt1 = dynamic_cast(filename->exprs[0]); + QVERIFY(txt1 != nullptr); + QCOMPARE(txt1->text, QString("out/")); + + auto conditional = dynamic_cast(filename->exprs[1]); + QVERIFY(conditional != nullptr); + QVERIFY(conditional->ifTrue != nullptr); + QVERIFY(conditional->ifFalse == nullptr); + + auto cond = dynamic_cast(conditional->condition); + QVERIFY(cond != nullptr); + QCOMPARE(cond->tag.text(), QString("tag")); + + auto ifTrue = dynamic_cast(conditional->ifTrue); + QVERIFY(ifTrue != nullptr); + QCOMPARE(ifTrue->exprs.count(), 3); + + auto ifTrue1 = dynamic_cast(ifTrue->exprs[0]); + QVERIFY(ifTrue1 != nullptr); + QCOMPARE(ifTrue1->text, QString("some ")); + + auto ifTrue2 = dynamic_cast(ifTrue->exprs[1]); + QVERIFY(ifTrue2 != nullptr); + QCOMPARE(ifTrue2->tag.text(), QString("tag")); + + auto ifTrue3 = dynamic_cast(ifTrue->exprs[2]); + QVERIFY(ifTrue3 != nullptr); + QCOMPARE(ifTrue3->text, QString(" is present/")); + + auto txt2 = dynamic_cast(filename->exprs[2]); + QVERIFY(txt2 != nullptr); + QCOMPARE(txt2->text, QString("image.png")); +} + +void FilenameParserTest::testParseConditionalNoCondition() +{ + FilenameParser parser(""); + parser.parseRoot(); + + QCOMPARE(parser.error(), QString("No condition found in conditional")); +} + +void FilenameParserTest::testParseConditionalNoContent() +{ + FilenameParser parser("<%condition%:?>"); + parser.parseRoot(); + + QCOMPARE(parser.error(), QString("Expected '?' after condition")); +} + +void FilenameParserTest::testParseConditionalUnterminated() +{ + FilenameParser parser("out/<\"tag\"?some tag is present:%artist%"); + parser.parseRoot(); + + QCOMPARE(parser.error(), QString("Expected '>' at the end of contional")); +} + + +void FilenameParserTest::testParseConditionTag() +{ + FilenameParser parser("\"my_tag\""); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto tagCond = dynamic_cast(cond); + QVERIFY(tagCond != nullptr); + QCOMPARE(tagCond->tag.text(), QString("my_tag")); +} + +void FilenameParserTest::testParseConditionTagWithoutQuotes() +{ + FilenameParser parser("my_tag"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto tagCond = dynamic_cast(cond); + QVERIFY(tagCond != nullptr); + QCOMPARE(tagCond->tag.text(), QString("my_tag")); +} + +void FilenameParserTest::testParseConditionToken() +{ + FilenameParser parser("%my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto tokenCond = dynamic_cast(cond); + QVERIFY(tokenCond != nullptr); + QCOMPARE(tokenCond->token, QString("my_token")); +} + +void FilenameParserTest::testParseConditionInvert() +{ + FilenameParser parser("!%my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto invertCond = dynamic_cast(cond); + QVERIFY(invertCond != nullptr); + + auto tokenCond = dynamic_cast(invertCond->node); + QVERIFY(tokenCond != nullptr); + QCOMPARE(tokenCond->token, QString("my_token")); +} + +void FilenameParserTest::testParseConditionOperator() +{ + FilenameParser parser("\"my_tag\" & %my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto opCond = dynamic_cast(cond); + QVERIFY(opCond != nullptr); + QCOMPARE(opCond->op, FilenameNodeConditionOp::Operator::And); + + auto left = dynamic_cast(opCond->left); + QVERIFY(left != nullptr); + QCOMPARE(left->tag.text(), QString("my_tag")); + + auto right = dynamic_cast(opCond->right); + QVERIFY(right != nullptr); + QCOMPARE(right->token, QString("my_token")); +} + +void FilenameParserTest::testParseConditionMixedOperators() +{ + FilenameParser parser("\"my_tag\" | %some_token% & !%my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto opCond = dynamic_cast(cond); + QVERIFY(opCond != nullptr); + QCOMPARE(opCond->op, FilenameNodeConditionOp::Operator::Or); + + auto right = dynamic_cast(opCond->right); + QVERIFY(right != nullptr); + QCOMPARE(right->op, FilenameNodeConditionOp::Operator::And); + + auto invert = dynamic_cast(right->right); + QVERIFY(invert != nullptr); +} + +void FilenameParserTest::testParseConditionNoOperator() +{ + FilenameParser parser("\"my_tag\" %my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto opCond = dynamic_cast(cond); + QVERIFY(opCond != nullptr); + QCOMPARE(opCond->op, FilenameNodeConditionOp::Operator::And); + + auto left = dynamic_cast(opCond->left); + QVERIFY(left != nullptr); + QCOMPARE(left->tag.text(), QString("my_tag")); + + auto right = dynamic_cast(opCond->right); + QVERIFY(right != nullptr); + QCOMPARE(right->token, QString("my_token")); +} + +void FilenameParserTest::testParseConditionTagParenthesis() +{ + FilenameParser parser("(\"my_tag\")"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto tagCond = dynamic_cast(cond); + QVERIFY(tagCond != nullptr); + QCOMPARE(tagCond->tag.text(), QString("my_tag")); +} + +void FilenameParserTest::testParseConditionTagParenthesisUnclosed() +{ + FilenameParser parser("(\"my_tag\""); + parser.parseCondition(); + + QCOMPARE(parser.error(), QString("Expected ')' after condition in parenthesis")); +} + +void FilenameParserTest::testParseConditionMixedParenthesis() +{ + FilenameParser parser("(\"my_tag\" | %some_token%) & %my_token%"); + auto cond = parser.parseCondition(); + QVERIFY(parser.error().isEmpty()); + + auto opCond = dynamic_cast(cond); + QVERIFY(opCond != nullptr); + QCOMPARE(opCond->op, FilenameNodeConditionOp::Operator::And); + + auto right = dynamic_cast(opCond->left); + QVERIFY(right != nullptr); + QCOMPARE(right->op, FilenameNodeConditionOp::Operator::Or); +} + + +QTEST_MAIN(FilenameParserTest) diff --git a/tests/src/filename/filename-parser-test.h b/tests/src/filename/filename-parser-test.h new file mode 100644 index 000000000..3c124e8f5 --- /dev/null +++ b/tests/src/filename/filename-parser-test.h @@ -0,0 +1,39 @@ +#ifndef FILENAME_PARSER_TEST_H +#define FILENAME_PARSER_TEST_H + +#include "test-suite.h" + + +class FilenameParserTest : public TestSuite +{ + Q_OBJECT + + private slots: + // Basic + void testParseEmpty(); + void testParseText(); + void testParseVariable(); + void testParseVariableWithOptions(); + void testParseMixed(); + + // Conditionals + void testParseConditional(); + void testParseConditionalLegacy(); + void testParseConditionalNoCondition(); + void testParseConditionalNoContent(); + void testParseConditionalUnterminated(); + + // Condition + void testParseConditionTag(); + void testParseConditionTagWithoutQuotes(); + void testParseConditionToken(); + void testParseConditionInvert(); + void testParseConditionOperator(); + void testParseConditionMixedOperators(); + void testParseConditionNoOperator(); + void testParseConditionTagParenthesis(); + void testParseConditionTagParenthesisUnclosed(); + void testParseConditionMixedParenthesis(); +}; + +#endif // FILENAME_PARSER_TEST_H diff --git a/tests/src/filename/filename-resolution-visitor-test.cpp b/tests/src/filename/filename-resolution-visitor-test.cpp new file mode 100644 index 000000000..c8ed38633 --- /dev/null +++ b/tests/src/filename/filename-resolution-visitor-test.cpp @@ -0,0 +1,54 @@ +#include "filename-resolution-visitor-test.h" +#include +#include +#include +#include "filename/filename-parser.h" +#include "filename/filename-resolution-visitor.h" + + +void FilenameResolutionVisitorTest::testEmpty() +{ + FilenameParser parser(""); + auto ast = parser.parseRoot(); + + FilenameResolutionVisitor resolutionVisitor; + auto results = resolutionVisitor.run(*ast); + + QCOMPARE(results, QSet()); +} + +void FilenameResolutionVisitorTest::testBasic() +{ + FilenameParser parser("out/%md5:opt%.%ext%"); + auto ast = parser.parseRoot(); + + FilenameResolutionVisitor resolutionVisitor; + auto results = resolutionVisitor.run(*ast); + + QCOMPARE(results, QSet() << "md5" << "ext"); +} + +void FilenameResolutionVisitorTest::testConditional() +{ + FilenameParser parser("out/<%id%?some tag is present:%rating%>/%md5%.%ext%"); + auto ast = parser.parseRoot(); + + FilenameResolutionVisitor resolutionVisitor; + auto results = resolutionVisitor.run(*ast); + + QCOMPARE(results, QSet() << "id" << "rating" << "md5" << "ext"); +} + +void FilenameResolutionVisitorTest::testDuplicates() +{ + FilenameParser parser("%md5%/file-%md5:opt%.%ext%"); + auto ast = parser.parseRoot(); + + FilenameResolutionVisitor resolutionVisitor; + auto results = resolutionVisitor.run(*ast); + + QCOMPARE(results, QSet() << "md5" << "ext"); +} + + +QTEST_MAIN(FilenameResolutionVisitorTest) diff --git a/tests/src/filename/filename-resolution-visitor-test.h b/tests/src/filename/filename-resolution-visitor-test.h new file mode 100644 index 000000000..fb087c47b --- /dev/null +++ b/tests/src/filename/filename-resolution-visitor-test.h @@ -0,0 +1,18 @@ +#ifndef FILENAME_RESOLUTION_VISITOR_TEST_H +#define FILENAME_RESOLUTION_VISITOR_TEST_H + +#include "test-suite.h" + + +class FilenameResolutionVisitorTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testEmpty(); + void testBasic(); + void testConditional(); + void testDuplicates(); +}; + +#endif // FILENAME_RESOLUTION_VISITOR_TEST_H diff --git a/tests/src/functions-test.cpp b/tests/src/functions-test.cpp index ff2ee1868..8fad938db 100644 --- a/tests/src/functions-test.cpp +++ b/tests/src/functions-test.cpp @@ -12,8 +12,9 @@ QDateTime fileCreationDate(const QString &path) return fi.created(); #else QDateTime d = fi.birthTime(); - if (d.isValid()) + if (d.isValid()) { return d; + } return fi.metadataChangeTime(); #endif } @@ -38,6 +39,7 @@ void FunctionsTest::testFixFilenameWindows() assertFixFilename(0, "image.jpg", "C:\\test\\", "image.jpg"); assertFixFilename(0, "image", "C:\\test\\", "image"); assertFixFilename(0, "folder\\image.jpg", "C:\\test\\", "folder\\image.jpg"); + assertFixFilename(0, "folder...\\image.jpg", "C:\\test\\", "folder\\image.jpg"); } void FunctionsTest::testFixFilenameLinux() @@ -51,8 +53,9 @@ void FunctionsTest::testFixFilenameLinux() static QByteArray readFile(const QString &path) { QFile f(path); - if (!f.open(QFile::ReadOnly)) + if (!f.open(QFile::ReadOnly)) { return QByteArray(); + } return f.readAll(); } @@ -66,15 +69,17 @@ void FunctionsTest::testGetExtensionFromHeader() QCOMPARE(getExtensionFromHeader(readFile("tests/resources/minimal/mp4.mp4")), QString("mp4")); QCOMPARE(getExtensionFromHeader(readFile("tests/resources/minimal/swf.swf")), QString("swf")); QCOMPARE(getExtensionFromHeader(readFile("tests/resources/minimal/ico.ico")), QString("ico")); + QCOMPARE(getExtensionFromHeader(readFile("tests/resources/minimal/txt.txt")), QString()); } static QFont makeFont(const QString &name, int size, bool usePixels, int weight, QFont::Style style) { QFont font(name); - if (usePixels) + if (usePixels) { font.setPixelSize(size); - else + } else { font.setPointSize(size); + } font.setWeight(weight); font.setStyle(style); return font; @@ -324,6 +329,39 @@ void FunctionsTest::testFixCloudflareEmails() QCOMPARE(fixCloudflareEmails(R"(Koshimizu Sachiko on [email protected])"), QString("Koshimizu Sachiko on Project-iM@S")); } +void FunctionsTest::testGetFileMd5() +{ + QCOMPARE(getFileMd5(QString()), QString()); + QCOMPARE(getFileMd5("non_existing_path.txt"), QString()); + + QTemporaryFile file; + QVERIFY(file.open()); + file.write("test"); + file.seek(0); + + QCOMPARE(getFileMd5(file.fileName()), QString("098f6bcd4621d373cade4e832627b4f6")); // md5("test") +} +void FunctionsTest::testGetFilenameMd5() +{ + QCOMPARE(getFilenameMd5("", "%md5%.%ext%"), QString()); + QCOMPARE(getFilenameMd5("lol.jpg", "%md5%.%ext%"), QString()); + QCOMPARE(getFilenameMd5("test/098f6bcd4621d373cade4e832627b4f6.jpg", "%md5%.%ext%"), QString()); + + QCOMPARE(getFilenameMd5("098f6bcd4621d373cade4e832627b4f6", "%md5%"), QString("098f6bcd4621d373cade4e832627b4f6")); + QCOMPARE(getFilenameMd5("098f6bcd4621d373cade4e832627b4f6.jpg", "%md5%.%ext%"), QString("098f6bcd4621d373cade4e832627b4f6")); + QCOMPARE(getFilenameMd5("test/098f6bcd4621d373cade4e832627b4f6.jpg", "%artist%/%md5%.%ext%"), QString("098f6bcd4621d373cade4e832627b4f6")); +} + +void FunctionsTest::testRemoveCacheBuster() +{ + QCOMPARE(removeCacheBuster(QUrl("https://test.com")), QUrl("https://test.com")); + QCOMPARE(removeCacheBuster(QUrl("https://test.com?string")), QUrl("https://test.com?string")); + QCOMPARE(removeCacheBuster(QUrl("https://test.com?1234")), QUrl("https://test.com")); + QCOMPARE(removeCacheBuster(QUrl("https://test.com/path")), QUrl("https://test.com/path")); + QCOMPARE(removeCacheBuster(QUrl("https://test.com/path?string")), QUrl("https://test.com/path?string")); + QCOMPARE(removeCacheBuster(QUrl("https://test.com/path?1234")), QUrl("https://test.com/path")); +} + void FunctionsTest::assertFixFilename(int platform, const QString &filename, const QString &path, const QString &expected) { diff --git a/tests/src/functions-test.h b/tests/src/functions-test.h index 237b02b43..1f10b582e 100644 --- a/tests/src/functions-test.h +++ b/tests/src/functions-test.h @@ -31,6 +31,9 @@ class FunctionsTest : public TestSuite void testGetExternalLogFilesSuffixes(); void testFixCloudflareEmail(); void testFixCloudflareEmails(); + void testGetFileMd5(); + void testGetFilenameMd5(); + void testRemoveCacheBuster(); protected: void assertFixFilename(int platform, const QString &filename, const QString &path, const QString &expected); diff --git a/tests/src/integration/behoimi-test.cpp b/tests/src/integration/behoimi-test.cpp index 9c9f3a321..3a39a342e 100644 --- a/tests/src/integration/behoimi-test.cpp +++ b/tests/src/integration/behoimi-test.cpp @@ -12,8 +12,7 @@ void BehoimiTest::testHtml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } @@ -31,8 +30,7 @@ void BehoimiTest::testXml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } @@ -50,8 +48,7 @@ void BehoimiTest::testJson() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } diff --git a/tests/src/integration/booru-org-test.cpp b/tests/src/integration/booru-org-test.cpp index 8cb8f6232..f70699a51 100644 --- a/tests/src/integration/booru-org-test.cpp +++ b/tests/src/integration/booru-org-test.cpp @@ -12,8 +12,7 @@ void BooruOrgTest::testHtml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } diff --git a/tests/src/integration/danbooru-test.cpp b/tests/src/integration/danbooru-test.cpp index 2526354b2..b5016d7e8 100644 --- a/tests/src/integration/danbooru-test.cpp +++ b/tests/src/integration/danbooru-test.cpp @@ -12,8 +12,7 @@ void DanbooruTest::testHtml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } @@ -31,8 +30,7 @@ void DanbooruTest::testXml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } diff --git a/tests/src/integration/derpibooru-test.cpp b/tests/src/integration/derpibooru-test.cpp index 0132a225c..beaf95e86 100644 --- a/tests/src/integration/derpibooru-test.cpp +++ b/tests/src/integration/derpibooru-test.cpp @@ -12,8 +12,7 @@ void DerpibooruTest::testHtml() // Convert results QList ids; ids.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { ids.append(img->id()); } @@ -31,8 +30,7 @@ void DerpibooruTest::testJson() // Convert results QList ids; ids.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { ids.append(img->id()); } diff --git a/tests/src/integration/e621-test.cpp b/tests/src/integration/e621-test.cpp index cf5acd224..83bf8b701 100644 --- a/tests/src/integration/e621-test.cpp +++ b/tests/src/integration/e621-test.cpp @@ -13,8 +13,7 @@ void E621Test::testSwfUrls() QStringList md5s, urls; md5s.reserve(images.count()); urls.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); urls.append(img->url().toString()); } diff --git a/tests/src/integration/gelbooru-test.cpp b/tests/src/integration/gelbooru-test.cpp index f591e8cbf..91b970823 100644 --- a/tests/src/integration/gelbooru-test.cpp +++ b/tests/src/integration/gelbooru-test.cpp @@ -12,8 +12,7 @@ void GelbooruTest::testHtml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } @@ -31,8 +30,7 @@ void GelbooruTest::testXml() // Convert results QStringList md5s; md5s.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { md5s.append(img->md5()); } diff --git a/tests/src/integration/integration-test-suite.cpp b/tests/src/integration/integration-test-suite.cpp index 91dc73d4f..f351b3f9b 100755 --- a/tests/src/integration/integration-test-suite.cpp +++ b/tests/src/integration/integration-test-suite.cpp @@ -26,8 +26,9 @@ QList IntegrationTestSuite::getImages(const QString &site, const QString setupSite(site, source); // Setup network - if (!file.isEmpty()) - { CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); } + if (!file.isEmpty()) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); + } QSettings settings("tests/resources/sites/" + site + "/" + source + "/settings.ini", QSettings::IniFormat); settings.setValue("download/throttle_retry", 0); @@ -69,8 +70,9 @@ QList IntegrationTestSuite::getImages(const QString &site, const QString // Wait for downloader QSignalSpy spy(m_downloader, SIGNAL(finishedImages(QList>))); m_downloader->getImages(); - if (!spy.wait()) + if (!spy.wait()) { return result; + } // Get results QList arguments = spy.takeFirst(); @@ -78,8 +80,7 @@ QList IntegrationTestSuite::getImages(const QString &site, const QString // Convert results result.reserve(variants.count()); - for (const QVariant &variant : variants) - { + for (const QVariant &variant : variants) { QSharedPointer img = variant.value>(); result.append(img.data()); } @@ -102,8 +103,9 @@ QList IntegrationTestSuite::getPageTags(const QString &site, const QString m_filesToRemove.append(settings.fileName()); // Setup network - if (!file.isEmpty()) - { CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); } + if (!file.isEmpty()) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); + } m_profile = new Profile("tests/resources/"); m_source = new Source(m_profile, "tests/resources/sites/" + site); @@ -135,8 +137,9 @@ QList IntegrationTestSuite::getPageTags(const QString &site, const QString // Wait for downloader QSignalSpy spy(m_downloader, SIGNAL(finishedTags(QList))); m_downloader->getPageTags(); - if (!spy.wait()) + if (!spy.wait()) { return result; + } // Get results QList arguments = spy.takeFirst(); @@ -144,8 +147,7 @@ QList IntegrationTestSuite::getPageTags(const QString &site, const QString // Convert results result.reserve(variants.count()); - for (const QVariant &variant : variants) - { + for (const QVariant &variant : variants) { Tag tag = variant.value(); result.append(tag); } @@ -168,8 +170,9 @@ QList IntegrationTestSuite::getTags(const QString &site, const QString &sou m_filesToRemove.append(settings.fileName()); // Setup network - if (!file.isEmpty()) - { CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); } + if (!file.isEmpty()) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/" + source + "/" + file); + } m_profile = new Profile("tests/resources/"); m_source = new Source(m_profile, "tests/resources/sites/" + site); @@ -183,42 +186,41 @@ QList IntegrationTestSuite::getTags(const QString &site, const QString &sou // Wait for tag api QSignalSpy spy(&tagApi, SIGNAL(finishedLoading(TagApi*, TagApi::LoadResult))); tagApi.load(false); - if (!spy.wait()) + if (!spy.wait()) { return result; + } // Check result type QList arguments = spy.takeFirst(); TagApi::LoadResult res = arguments.at(1).value(); - if (res != TagApi::LoadResult::Ok) + if (res != TagApi::LoadResult::Ok) { return result; + } return tagApi.tags(); } void IntegrationTestSuite::cleanup() { - if (m_downloader != nullptr) - { + if (m_downloader != nullptr) { m_downloader->deleteLater(); m_downloader = nullptr; } - if (m_profile != nullptr) - { + if (m_profile != nullptr) { delete m_profile; m_profile = nullptr; } - if (m_source != nullptr) - { + if (m_source != nullptr) { delete m_source; m_source = nullptr; } - if (m_site != nullptr) - { + if (m_site != nullptr) { delete m_site; m_site = nullptr; } - for (const QString &file : m_filesToRemove) - { QFile(file).remove(); } + for (const QString &file : m_filesToRemove) { + QFile(file).remove(); + } m_filesToRemove.clear(); } diff --git a/tests/src/integration/zerochan-test.cpp b/tests/src/integration/zerochan-test.cpp index 78209e02d..6930da71b 100644 --- a/tests/src/integration/zerochan-test.cpp +++ b/tests/src/integration/zerochan-test.cpp @@ -12,8 +12,7 @@ void ZerochanTest::testHtml() // Convert results QList ids; ids.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { ids.append(img->id()); } @@ -31,8 +30,7 @@ void ZerochanTest::testRss() // Convert results QList ids; ids.reserve(images.count()); - for (Image *img : images) - { + for (Image *img : images) { ids.append(img->id()); } diff --git a/tests/src/language-loader-test.cpp b/tests/src/language-loader-test.cpp new file mode 100644 index 000000000..5c6adfbfb --- /dev/null +++ b/tests/src/language-loader-test.cpp @@ -0,0 +1,48 @@ +#include "language-loader-test.h" +#include +#include "language-loader.h" + + +void LanguageLoaderTest::testInvalid() +{ + LanguageLoader loader("non_existing_dir/"); + QMap languages = loader.getAllLanguages(); + + QCOMPARE(languages.keys(), QList() << ""); + QCOMPARE(languages[""], QString("English")); +} + +void LanguageLoaderTest::testValid() +{ + LanguageLoader loader("tests/resources/languages/"); + QMap languages = loader.getAllLanguages(); + + QCOMPARE(languages.keys(), QList() << "English" << "French"); + QCOMPARE(languages["English"], QString("English")); + QCOMPARE(languages["French"], QString("French - Français")); +} + +void LanguageLoaderTest::testSetLanguage() +{ + LanguageLoader loader("tests/resources/languages/"); + + // The first call should not have any impact because the translators are not installed yet + QVERIFY(loader.setLanguage("French")); + QCOMPARE(tr("Translation test"), QString("Translation test")); + + // Once installed, the translations should immediately be effective + QVERIFY(loader.install(qApp)); + QCOMPARE(tr("Translation test"), QString("Test de traduction")); + + // Another call to setLanguage should not require to re-install translators + QVERIFY(loader.setLanguage("English")); + QCOMPARE(tr("Translation test"), QString("Translation test")); + + // Uninstalling the translator should restore the original language + QVERIFY(loader.setLanguage("French")); + QVERIFY(loader.uninstall(qApp)); + QCOMPARE(tr("Translation test"), QString("Translation test")); +} + + +QTEST_MAIN(LanguageLoaderTest) diff --git a/tests/src/language-loader-test.h b/tests/src/language-loader-test.h new file mode 100644 index 000000000..a50f3f448 --- /dev/null +++ b/tests/src/language-loader-test.h @@ -0,0 +1,17 @@ +#ifndef LANGUAGE_LOADER_TEST_H +#define LANGUAGE_LOADER_TEST_H + +#include "test-suite.h" + + +class LanguageLoaderTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testInvalid(); + void testValid(); + void testSetLanguage(); +}; + +#endif // LANGUAGE_LOADER_TEST_H diff --git a/tests/src/loader/pack-loader-test.cpp b/tests/src/loader/pack-loader-test.cpp new file mode 100644 index 000000000..38ca0bc15 --- /dev/null +++ b/tests/src/loader/pack-loader-test.cpp @@ -0,0 +1,152 @@ +#include "pack-loader-test.h" +#include +#include +#include "custom-network-access-manager.h" +#include "loader/pack-loader.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void PackLoaderTest::init() +{ + m_profile = new Profile("tests/resources/"); +} + +void PackLoaderTest::cleanup() +{ + m_profile->deleteLater(); +} + + +void PackLoaderTest::testGetQuery() +{ + DownloadQueryGroup query(QStringList() << "search", 1, 10, 20, QStringList(), false, nullptr, "%md5%.%ext%", ""); + PackLoader loader(nullptr, query, 100, nullptr); + + QCOMPARE(loader.query(), query); +} + +void PackLoaderTest::testBasic() +{ + setupSource("Danbooru (2.0)"); + setupSite("Danbooru (2.0)", "danbooru.donmai.us"); + + Source source(m_profile, "tests/resources/sites/Danbooru (2.0)"); + Site site("danbooru.donmai.us", &source); + + // Login first + QSignalSpy spy(&site, SIGNAL(loggedIn(Site*, Site::LoginResult))); + QTimer::singleShot(0, &site, SLOT(login())); + QVERIFY(spy.wait()); + + // 2 packs of 9 from 8 pages of 2 + for (int i = 1; i <= 8; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/pack-loader-2-" + QString::number(i) + ".xml"); + } + QCOMPARE(getResults(m_profile, &site, "filesize:<200KB", 2, 15, 9, true), QList() << 9 << 6); + + // 5 packs of 3 from 7 pages of 2 + for (int i = 1; i <= 7; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/pack-loader-2-" + QString::number(i) + ".xml"); + } + QCOMPARE(getResults(m_profile, &site, "filesize:<200KB", 2, 13, 3, true), QList() << 3 << 3 << 3 << 3 << 1); + + // 3 packs of 6 from 1 page of 20 + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/pack-loader-20-1.xml"); + QCOMPARE(getResults(m_profile, &site, "filesize:<200KB", 20, 15, 6, true), QList() << 6 << 6 << 3); + + // 1 pack of 100 from 3 pages of 1 + for (int i = 1; i <= 3; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/pack-loader-1-" + QString::number(i) + ".xml"); + } + QCOMPARE(getResults(m_profile, &site, "filesize:<200KB", 1, 3, 100, true), QList() << 3); +} + +void PackLoaderTest::testWrongResultsCount() +{ + setupSource("Gelbooru (0.2)"); + setupSite("Gelbooru (0.2)", "gelbooru.com"); + + Source source(m_profile, "tests/resources/sites/Gelbooru (0.2)"); + Site site("gelbooru.com", &source); + + // Login first + QSignalSpy spy(&site, SIGNAL(loggedIn(Site*, Site::LoginResult))); + QTimer::singleShot(0, &site, SLOT(login())); + QVERIFY(spy.wait()); + + // 4 packs of 90 from 7 pages of 50 + for (int i = 1; i <= 7; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/gelbooru.com/pack-loader-" + QString::number(i) + ".html"); + } + QCOMPARE(getResults(m_profile, &site, "fav:123", 20, 400, 90, true), QList() << 90 << 90 << 90 << 64); + + // 5 packs of 30 from 3 pages of 50 + for (int i = 1; i <= 3; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/gelbooru.com/pack-loader-" + QString::number(i) + ".html"); + } + QCOMPARE(getResults(m_profile, &site, "fav:123", 20, 140, 30, true), QList() << 30 << 30 << 30 << 30 << 20); + + // 3 packs of 50 from 3 pages of 50 + for (int i = 1; i <= 3; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/gelbooru.com/pack-loader-" + QString::number(i) + ".html"); + } + QCOMPARE(getResults(m_profile, &site, "fav:123", 200, 140, 50, true), QList() << 50 << 50 << 40); + + // 1 pack of 5 from 1 page of 50 + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/gelbooru.com/pack-loader-1.html"); + QCOMPARE(getResults(m_profile, &site, "fav:123", 1, 5, 100, true), QList() << 5); +} + +void PackLoaderTest::testGalleries() +{ + setupSource("E-Hentai"); + setupSite("E-Hentai", "e-hentai.org"); + + Source source(m_profile, "tests/resources/sites/E-Hentai"); + Site site("e-hentai.org", &source); + + // Login first + QSignalSpy spy(&site, SIGNAL(loggedIn(Site*, Site::LoginResult))); + QTimer::singleShot(0, &site, SLOT(login())); + QVERIFY(spy.wait()); + + // 2 packs of 30 from 1 gallery of 31 + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-list.html"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-gallery-1-1.html"); + QCOMPARE(getResults(m_profile, &site, "tomose shunsaku", 1, 1, 30, true), QList() << 30 << 1); + + // 2 packs of 50 from 3 galleries of 31, 32, 24 + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-list.html"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-gallery-1-1.html"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-gallery-2-1.html"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-gallery-3-1.html"); + QCOMPARE(getResults(m_profile, &site, "tomose shunsaku", 1, 3, 50, true), QList() << 50 << 37); + + // 1 pack of 50 from 1 gallery of 31 (images) + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-list.html"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/e-hentai.org/pack-loader-gallery-1-1.html"); + QCOMPARE(getResults(m_profile, &site, "tomose shunsaku", 1, 3, 50, false), QList() << 3); +} + + +QList PackLoaderTest::getResults(Profile *profile, Site *site, QString search, int perPage, int total, int packSize, bool galleriesCountAsOne) +{ + QList ret; + + DownloadQueryGroup query(QStringList() << search, 1, perPage, total, QStringList(), false, site, "%md5%.%ext%", ""); + query.galleriesCountAsOne = galleriesCountAsOne; + + PackLoader loader(profile, query, packSize, nullptr); + loader.start(); + while (loader.hasNext()) { + auto images = loader.next(); + ret.append(images.count()); + } + + return ret; +} + + +QTEST_MAIN(PackLoaderTest) diff --git a/tests/src/loader/pack-loader-test.h b/tests/src/loader/pack-loader-test.h new file mode 100644 index 000000000..a0191d074 --- /dev/null +++ b/tests/src/loader/pack-loader-test.h @@ -0,0 +1,30 @@ +#ifndef PACK_LOADER_TEST_H +#define PACK_LOADER_TEST_H + +#include "test-suite.h" + + +class Profile; +class Site; + +class PackLoaderTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testGetQuery(); + void testBasic(); + void testWrongResultsCount(); + void testGalleries(); + + protected: + QList getResults(Profile *profile, Site *site, QString search, int perPage, int total, int packSize, bool galleriesCountAsOne); + + private: + Profile *m_profile; +}; + +#endif // PACK_LOADER_TEST_H diff --git a/tests/src/loader/token-test.cpp b/tests/src/loader/token-test.cpp index d81319949..76ee297b6 100644 --- a/tests/src/loader/token-test.cpp +++ b/tests/src/loader/token-test.cpp @@ -35,5 +35,17 @@ void TokenTest::testLazyWithoutCaching() QCOMPARE(val, 2); } +void TokenTest::testCompare() +{ + QVERIFY(Token(13) == Token(13)); + QVERIFY(Token(13) != Token(17)); + + QVERIFY(Token("test") == Token("test")); + QVERIFY(Token("test") != Token("not_test")); + + QVERIFY(Token(QStringList() << "1" << "2") == Token(QStringList() << "1" << "2")); + QVERIFY(Token(QStringList() << "1" << "2") != Token(QStringList() << "1" << "2" << "3")); +} + QTEST_MAIN(TokenTest) diff --git a/tests/src/loader/token-test.h b/tests/src/loader/token-test.h index 167882699..ce77d2d80 100644 --- a/tests/src/loader/token-test.h +++ b/tests/src/loader/token-test.h @@ -12,6 +12,7 @@ class TokenTest : public TestSuite void testLazyNotCalled(); void testLazyWithCaching(); void testLazyWithoutCaching(); + void testCompare(); }; #endif // TOKEN_TEST_H diff --git a/tests/src/login/http-login-test.cpp b/tests/src/login/http-login-test.cpp new file mode 100644 index 000000000..5ed294add --- /dev/null +++ b/tests/src/login/http-login-test.cpp @@ -0,0 +1,86 @@ +#include "login/http-login-test.h" +#include +#include +#include +#include "auth/auth-const-field.h" +#include "auth/auth-field.h" +#include "auth/http-auth.h" +#include "login/http-get-login.h" +#include "login/http-post-login.h" +#include "mixed-settings.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void HttpLoginTest::init() +{ + m_profile = new Profile("tests/resources/settings.ini"); + m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); + m_site = new Site("danbooru.donmai.us", m_source); +} + +void HttpLoginTest::cleanup() +{ + m_profile->deleteLater(); + m_source->deleteLater(); + m_site->deleteLater(); +} + + +void HttpLoginTest::testNonTestable() +{ + QList fields; + HttpAuth auth("url", "", fields, ""); + HttpGetLogin login(&auth, m_site, &m_manager, m_site->settings()); + + QVERIFY(!login.isTestable()); +} + +template +void testLogin(const QString &type, const QString &url, Login::Result expected, Site *site, CustomNetworkAccessManager *manager) +{ + // Clear all cookies + manager->setCookieJar(new QNetworkCookieJar(manager)); + + QList fields; + HttpAuth auth(type, "/login", fields, "test_cookie"); + T login(&auth, site, manager, site->settings()); + + QVERIFY(login.isTestable()); + + CustomNetworkAccessManager::NextFiles.enqueue(url); + + QSignalSpy spy(&login, SIGNAL(loggedIn(Login::Result))); + login.login(); + QVERIFY(spy.wait()); + + QList arguments = spy.takeFirst(); + Login::Result result = arguments.at(0).value(); + + QCOMPARE(result, expected); +} + +void HttpLoginTest::testLoginSuccess() +{ + testLogin("get", "cookie", Login::Result::Success, m_site, &m_manager); + testLogin("post", "cookie", Login::Result::Success, m_site, &m_manager); +} + +void HttpLoginTest::testLoginFailure() +{ + testLogin("get", "404", Login::Result::Failure, m_site, &m_manager); + testLogin("post", "404", Login::Result::Failure, m_site, &m_manager); +} + +void HttpLoginTest::testDoubleLogin() +{ + testLogin("get", "cookie", Login::Result::Success, m_site, &m_manager); + testLogin("get", "cookie", Login::Result::Success, m_site, &m_manager); + + testLogin("post", "cookie", Login::Result::Success, m_site, &m_manager); + testLogin("post", "cookie", Login::Result::Success, m_site, &m_manager); +} + + +QTEST_MAIN(HttpLoginTest) diff --git a/tests/src/login/http-login-test.h b/tests/src/login/http-login-test.h new file mode 100644 index 000000000..f79aefe8c --- /dev/null +++ b/tests/src/login/http-login-test.h @@ -0,0 +1,33 @@ +#ifndef HTTP_LOGIN_TEST_H +#define HTTP_LOGIN_TEST_H + +#include +#include "custom-network-access-manager.h" +#include "test-suite.h" + + +class Profile; +class Site; +class Source; + +class HttpLoginTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testNonTestable(); + void testLoginSuccess(); + void testLoginFailure(); + void testDoubleLogin(); + + private: + Profile *m_profile; + Source *m_source; + Site *m_site; + CustomNetworkAccessManager m_manager; +}; + +#endif // HTTP_LOGIN_TEST_H diff --git a/tests/src/login/oauth2-login-test.cpp b/tests/src/login/oauth2-login-test.cpp new file mode 100644 index 000000000..26eebab95 --- /dev/null +++ b/tests/src/login/oauth2-login-test.cpp @@ -0,0 +1,80 @@ +#include "login/oauth2-login-test.h" +#include +#include +#include "auth/oauth2-auth.h" +#include "login/oauth2-login.h" +#include "mixed-settings.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void OAuth2LoginTest::init() +{ + m_profile = new Profile("tests/resources/settings.ini"); + m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); + m_site = new Site("danbooru.donmai.us", m_source); +} + +void OAuth2LoginTest::cleanup() +{ + m_profile->deleteLater(); + m_source->deleteLater(); + m_site->deleteLater(); +} + + +void OAuth2LoginTest::testNonTestable() +{ + OAuth2Auth auth("oauth2", "password", ""); + OAuth2Login login(&auth, m_site, &m_manager, m_site->settings()); + + QVERIFY(!login.isTestable()); +} + +void testLogin(const QString &type, const QString &url, Login::Result expected, const QString &expectedHeader, Site *site, CustomNetworkAccessManager *manager) +{ + MixedSettings *settings = site->settings(); + settings->setValue("auth/consumerKey", "consumerKey"); + settings->setValue("auth/consumerSecret", "consumerSecret"); + + OAuth2Auth auth("oauth2", type, "/token"); + OAuth2Login login(&auth, site, manager, settings); + + QVERIFY(login.isTestable()); + + CustomNetworkAccessManager::NextFiles.enqueue(url); + + QSignalSpy spy(&login, SIGNAL(loggedIn(Login::Result))); + login.login(); + QVERIFY(spy.wait()); + + QList arguments = spy.takeFirst(); + Login::Result result = arguments.at(0).value(); + + QCOMPARE(result, expected); + + if (!expectedHeader.isEmpty()) { + QNetworkRequest req; + login.complementRequest(&req); + + QCOMPARE(QString(req.rawHeader("Authorization")), expectedHeader); + } +} + +void OAuth2LoginTest::testLoginSuccess() +{ + testLogin("header_basic", "tests/resources/oauth2/ok.json", Login::Result::Success, "Bearer test_token", m_site, &m_manager); + testLogin("client_credentials", "tests/resources/oauth2/ok_in_response.json", Login::Result::Success, "Bearer test_token", m_site, &m_manager); + testLogin("password", "tests/resources/oauth2/ok.json", Login::Result::Success, "Bearer test_token", m_site, &m_manager); +} + +void OAuth2LoginTest::testLoginFailure() +{ + testLogin("header_basic", "404", Login::Result::Failure, QString(), m_site, &m_manager); + testLogin("client_credentials", "tests/resources/oauth2/no_token_type.json", Login::Result::Failure, QString(), m_site, &m_manager); + testLogin("password", "tests/resources/oauth2/wrong_token_type.json", Login::Result::Failure, QString(), m_site, &m_manager); +} + + +QTEST_MAIN(OAuth2LoginTest) diff --git a/tests/src/login/oauth2-login-test.h b/tests/src/login/oauth2-login-test.h new file mode 100644 index 000000000..d710f8ada --- /dev/null +++ b/tests/src/login/oauth2-login-test.h @@ -0,0 +1,32 @@ +#ifndef OAUTH2_LOGIN_TEST_H +#define OAUTH2_LOGIN_TEST_H + +#include +#include "custom-network-access-manager.h" +#include "test-suite.h" + + +class Profile; +class Site; +class Source; + +class OAuth2LoginTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testNonTestable(); + void testLoginSuccess(); + void testLoginFailure(); + + private: + Profile *m_profile; + Source *m_source; + Site *m_site; + CustomNetworkAccessManager m_manager; +}; + +#endif // OAUTH2_LOGIN_TEST_H diff --git a/tests/src/login/url-login-test.cpp b/tests/src/login/url-login-test.cpp new file mode 100644 index 000000000..1553942da --- /dev/null +++ b/tests/src/login/url-login-test.cpp @@ -0,0 +1,106 @@ +#include "login/url-login-test.h" +#include +#include +#include "auth/auth-const-field.h" +#include "auth/auth-field.h" +#include "auth/url-auth.h" +#include "login/url-login.h" +#include "mixed-settings.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void UrlLoginTest::init() +{ + m_profile = new Profile("tests/resources/settings.ini"); + m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); + m_site = new Site("danbooru.donmai.us", m_source); +} + +void UrlLoginTest::cleanup() +{ + MixedSettings *settings = m_site->settings(); + settings->setValue("login/type", "url"); + settings->sync(); + + m_profile->deleteLater(); + m_source->deleteLater(); + m_site->deleteLater(); +} + + +void UrlLoginTest::testNonTestable() +{ + QList fields; + UrlAuth auth("url", fields, 0); + UrlLogin login(&auth, m_site, &m_manager, m_site->settings()); + + QVERIFY(!login.isTestable()); +} + +void UrlLoginTest::testLoginSuccess() +{ + MixedSettings *settings = m_site->settings(); + settings->setValue("login/type", "disabled"); + m_site->loadConfig(); + + QList fields; + UrlAuth auth("url", fields, 10); + UrlLogin login(&auth, m_site, &m_manager, settings); + + QVERIFY(login.isTestable()); + + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/results.xml"); + + QSignalSpy spy(&login, SIGNAL(loggedIn(Login::Result))); + login.login(); + QVERIFY(spy.wait()); + + QList arguments = spy.takeFirst(); + Login::Result result = arguments.at(0).value(); + + QCOMPARE(result, Login::Success); +} + +void UrlLoginTest::testLoginFailure() +{ + MixedSettings *settings = m_site->settings(); + settings->setValue("login/type", "disabled"); + m_site->loadConfig(); + + QList fields; + UrlAuth auth("url", fields, 10); + UrlLogin login(&auth, m_site, &m_manager, settings); + + QVERIFY(login.isTestable()); + + for (int i = 0; i < 3; ++i) { + CustomNetworkAccessManager::NextFiles.enqueue("404"); + } + + QSignalSpy spy(&login, SIGNAL(loggedIn(Login::Result))); + login.login(); + QVERIFY(spy.wait()); + + QList arguments = spy.takeFirst(); + Login::Result result = arguments.at(0).value(); + + QCOMPARE(result, Login::Failure); +} + +void UrlLoginTest::testComplementUrl() +{ + QList fields; + fields.append(new AuthConstField("a", "1")); + fields.append(new AuthConstField("b", "2")); + + UrlAuth auth("url", fields, 10); + UrlLogin login(&auth, m_site, &m_manager, m_site->settings()); + + QCOMPARE(login.complementUrl("/"), QString("/?a=1&b=2")); + QCOMPARE(login.complementUrl("/?test=1"), QString("/?test=1&a=1&b=2")); +} + + +QTEST_MAIN(UrlLoginTest) diff --git a/tests/src/login/url-login-test.h b/tests/src/login/url-login-test.h new file mode 100644 index 000000000..cad1daedc --- /dev/null +++ b/tests/src/login/url-login-test.h @@ -0,0 +1,33 @@ +#ifndef URL_LOGIN_TEST_H +#define URL_LOGIN_TEST_H + +#include +#include "custom-network-access-manager.h" +#include "test-suite.h" + + +class Profile; +class Site; +class Source; + +class UrlLoginTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testNonTestable(); + void testLoginSuccess(); + void testLoginFailure(); + void testComplementUrl(); + + private: + Profile *m_profile; + Source *m_source; + Site *m_site; + CustomNetworkAccessManager m_manager; +}; + +#endif // URL_LOGIN_TEST_H diff --git a/tests/src/main.cpp b/tests/src/main.cpp index 1404adcee..b78342a18 100644 --- a/tests/src/main.cpp +++ b/tests/src/main.cpp @@ -15,13 +15,13 @@ int main(int argc, char *argv[]) // A possible format to filter by test suite it to pass their names as arguments QStringList testSuites; QStringList arguments; - for (int i = 1; i < argc; ++i) - { + for (int i = 1; i < argc; ++i) { QString arg(argv[i]); - if (!arg.startsWith('-') && !arg.startsWith("test")) + if (!arg.startsWith('-') && !arg.startsWith("test")) { testSuites.append(arg); - else + } else { arguments.append(arg); + } } // Used for networking and finding test resource files @@ -29,10 +29,10 @@ int main(int argc, char *argv[]) // Run all selected test suites int errorCode = 0; - for (TestSuite *suite : TestSuite::getSuites()) - { - if (!testSuites.isEmpty() && !testSuites.contains(suite->metaObject()->className())) + for (TestSuite *suite : TestSuite::getSuites()) { + if (!testSuites.isEmpty() && !testSuites.contains(suite->metaObject()->className())) { continue; + } errorCode |= QTest::qExec(suite, arguments); std::cout << std::endl; diff --git a/tests/src/mixed-settings-test.cpp b/tests/src/mixed-settings-test.cpp index d4a201f80..9e78a0a75 100644 --- a/tests/src/mixed-settings-test.cpp +++ b/tests/src/mixed-settings-test.cpp @@ -108,5 +108,18 @@ void MixedSettingsTest::testChildKeys() QCOMPARE(settings.childKeys(), QStringList() << "child" << "parent"); } +void MixedSettingsTest::testChildKeysInGroup() +{ + MixedSettings settings(QList() << m_child << m_parent); + + m_child->setValue("other", "value"); + m_child->setValue("Group/child", "value"); + m_parent->setValue("Group/parent", "value"); + + settings.beginGroup("Group"); + QCOMPARE(settings.childKeys(), QStringList() << "child" << "parent"); + settings.endGroup(); +} + QTEST_MAIN(MixedSettingsTest) diff --git a/tests/src/mixed-settings-test.h b/tests/src/mixed-settings-test.h index 9ab7e3e0e..1952eaa3e 100644 --- a/tests/src/mixed-settings-test.h +++ b/tests/src/mixed-settings-test.h @@ -21,6 +21,7 @@ class MixedSettingsTest : public TestSuite void testSetValueOverrideParent(); void testSetValueOverrideDefault(); void testChildKeys(); + void testChildKeysInGroup(); private: QSettings *m_child; diff --git a/tests/src/models/favorite-test.cpp b/tests/src/models/favorite-test.cpp index b88ec370b..a201d57d0 100644 --- a/tests/src/models/favorite-test.cpp +++ b/tests/src/models/favorite-test.cpp @@ -3,6 +3,9 @@ #include #include "functions.h" #include "models/favorite.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" void FavoriteTest::testBasicConstructor() @@ -272,5 +275,25 @@ void FavoriteTest::testSortByLastViewed() QCOMPARE(favorites[2].getName(), QString("f1")); } +void FavoriteTest::testSerialization() +{ + Profile profile("tests/resources/"); + Source source(&profile, "tests/resources/sites/Danbooru (2.0)"); + Site site("danbooru.donmai.us", &source); + + QDateTime date = QDateTime::fromString("2016-07-02 16:35:12", "yyyy-MM-dd HH:mm:ss"); + Monitor monitor(&site, 60, date); + Favorite original("fate/stay_night", 50, date, QList() << monitor); + + QJsonObject json; + original.toJson(json); + + Favorite dest = Favorite::fromJson("", json, QMap {{ site.url(), &site }}); + + QCOMPARE(dest.getName(), original.getName()); + QCOMPARE(dest.getNote(), original.getNote()); + QCOMPARE(dest.getLastViewed(), original.getLastViewed()); +} + QTEST_MAIN(FavoriteTest) diff --git a/tests/src/models/favorite-test.h b/tests/src/models/favorite-test.h index ada4ee634..4296faad0 100644 --- a/tests/src/models/favorite-test.h +++ b/tests/src/models/favorite-test.h @@ -35,6 +35,7 @@ class FavoriteTest : public TestSuite void testSortByNote(); void testSortByName(); void testSortByLastViewed(); + void testSerialization(); }; #endif // FAVORITE_TEST_H diff --git a/tests/src/models/filename-test.cpp b/tests/src/models/filename-test.cpp index af8a1aa03..c34152b6a 100644 --- a/tests/src/models/filename-test.cpp +++ b/tests/src/models/filename-test.cpp @@ -12,8 +12,9 @@ void FilenameTest::init() { // Make tmp dir if not already existing QDir tmp("tests/resources/"); - if (!tmp.exists("tmp")) + if (!tmp.exists("tmp")) { tmp.mkdir("tmp"); + } m_details["md5"] = "1bc29b36f623ba82aaf6724fd3b16718"; m_details["ext"] = "jpg"; @@ -43,6 +44,7 @@ void FilenameTest::init() m_details["tags_meta"] = ""; m_details["created_at"] = "1471513944"; m_details["rating"] = "safe"; + m_details["name"] = "Test gallery name"; m_profile = new Profile("tests/resources/settings.ini"); m_settings = m_profile->getSettings(); @@ -58,7 +60,10 @@ void FilenameTest::init() m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); m_site = new Site("danbooru.donmai.us", m_source); + m_gallery = new Image(m_site, m_details, m_profile); + m_details.remove("name"); m_img = new Image(m_site, m_details, m_profile); + m_img->setParentGallery(QSharedPointer(m_gallery)); } void FilenameTest::cleanup() @@ -73,7 +78,7 @@ void FilenameTest::testDefaultConstructor() { Filename fn; - QCOMPARE(fn.getFormat().isEmpty(), true); + QCOMPARE(fn.format().isEmpty(), true); } void FilenameTest::testGetFormat() @@ -81,7 +86,7 @@ void FilenameTest::testGetFormat() QString format = "%md5%.%ext%"; Filename fn(format); - QCOMPARE(fn.getFormat(), format); + QCOMPARE(fn.format(), format); } void FilenameTest::testSetFormat() @@ -90,7 +95,7 @@ void FilenameTest::testSetFormat() Filename fn("%id%.%ext%"); fn.setFormat(format); - QCOMPARE(fn.getFormat(), format); + QCOMPARE(fn.format(), format); } void FilenameTest::testPathSimple() @@ -117,6 +122,16 @@ void FilenameTest::testPathKeepN() assertPath("%artist%/%copyright%/%character%/%md5%.%ext%", "artist1/crossover/character1/1bc29b36f623ba82aaf6724fd3b16718.jpg"); } +void FilenameTest::testPathSort() +{ + m_img->deleteLater(); + m_details["tags_copyright"] = "copyright2 copyright1"; + m_settings->setValue("Save/copyright_multiple", "keepAll"); + m_settings->setValue("Save/copyright_sort", "name"); + m_img = new Image(m_site, m_details, m_profile); + + assertPath("%copyright%", "copyright1 copyright2"); +} void FilenameTest::testPathKeepNThenAdd() { m_settings->setValue("Save/character_multiple", "keepNThenAdd"); @@ -215,85 +230,84 @@ void FilenameTest::testPathInvalidJavascript() void FilenameTest::testExpandTagSimple() { - assertExpand("<\"unknown\" is one of the image tags> %md5%.%ext%", - "image contains the tag tag1 %md5%.%ext%"); - assertExpand("<\"tag2\" is one of the image tags> %md5%.%ext%", - "tag2 is one of the image tags %md5%.%ext%"); - assertExpand("<\"tag2\" is one of the image tags> %md5%.%ext%", - "image contains the tag tag1tag2 is one of the image tags %md5%.%ext%"); - assertExpand("<\"unknown2\" is one of the image tags> %md5%.%ext%", - " %md5%.%ext%"); + assertPath("<\"unknown\" is one of the image tags> %md5%.%ext%", + "image contains the tag tag1 1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag2\" is one of the image tags> %md5%.%ext%", + "tag2 is one of the image tags 1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag2\" is one of the image tags> %md5%.%ext%", + "image contains the tag tag1tag2 is one of the image tags 1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"unknown2\" is one of the image tags> %md5%.%ext%", + "1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTagWithInvalidCharacter() { - assertExpand("<\"fate/stay_night\"/>%md5%.%ext%", "%md5%.%ext%"); + assertPath("<\"fate/stay_night\"/>%md5%.%ext%", "1bc29b36f623ba82aaf6724fd3b16718.jpg"); m_img->deleteLater(); m_details["tags_copyright"] = "fate/stay_night"; m_img = new Image(m_site, m_details, m_profile); - assertExpand("<\"fate/stay_night\"/>%md5%.%ext%", "fate_stay_night/%md5%.%ext%"); + assertPath("<\"fate/stay_night\"/>%md5%.%ext%", "fate_stay_night/1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTagInvert() { - assertExpand(" %md5%.%ext%", - "unknown is not one of the image tags %md5%.%ext%"); - assertExpand(" %md5%.%ext%", - "image does not contain the tag unknown %md5%.%ext%"); + assertPath(" %md5%.%ext%", + "unknown is not one of the image tags 1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath(" %md5%.%ext%", + "image does not contain the tag unknown 1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTagMultiple() { - assertExpand("<\"tag1\" \"tag2\"/>%md5%.%ext%", "tag1 tag2/%md5%.%ext%"); - assertExpand("<\"tag1\" \"tag4\"/>%md5%.%ext%", "%md5%.%ext%"); + assertPath("<\"tag1\" \"tag2\"/>%md5%.%ext%", "tag1 tag2/1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag1\" \"tag4\"/>%md5%.%ext%", "1bc29b36f623ba82aaf6724fd3b16718.jpg"); - assertExpand("<\"tag1\" !\"tag4\"/>%md5%.%ext%", "tag1 tag4/%md5%.%ext%"); - assertExpand("<\"tag1\" !\"tag2\"/>%md5%.%ext%", "%md5%.%ext%"); + assertPath("<\"tag1\" !\"tag4\"/>%md5%.%ext%", "tag1 tag4/1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag1\" !\"tag2\"/>%md5%.%ext%", "1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTagIgnore() { - assertExpand("<\"tag1\"folder1/>%md5%.%ext%", "tag1folder1/%md5%.%ext%"); - assertExpand("<-\"tag1\"folder1/>%md5%.%ext%", "folder1/%md5%.%ext%"); + assertPath("<\"tag1\"folder1/>%md5%.%ext%", "tag1folder1/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); + assertPath("<-\"tag1\"folder1/>%md5%.%ext%", "folder1/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); - assertExpand("<\"tag1\"\"tag2\"folder1/>%md5%.%ext%", "tag1tag2folder1/%md5%.%ext%"); - assertExpand("<-\"tag1\"-\"tag2\"folder1/>%md5%.%ext%", "folder1/%md5%.%ext%"); + assertPath("<\"tag1\"\"tag2\"folder1/>%md5%.%ext%", "tag1tag2folder1/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); + assertPath("<-\"tag1\"-\"tag2\"folder1/>%md5%.%ext%", "folder1/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); - assertExpand("<\"tag1\" \"tag2\"/>%md5%.%ext%", "tag1 tag2/%md5%.%ext%"); - assertExpand("<\"tag1\"-\"tag2\"/>%md5%.%ext%", "tag1/%md5%.%ext%"); - assertExpand("<-\"tag1\"\"tag2\"/>%md5%.%ext%", "tag2/%md5%.%ext%"); - assertExpand("<-\"tag1\"-\"tag2\"/>%md5%.%ext%", "/%md5%.%ext%"); + assertPath("<\"tag1\" \"tag2\"/>%md5%.%ext%", "tag1 tag2/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); + assertPath("<\"tag1\"-\"tag2\"/>%md5%.%ext%", "tag1/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); + assertPath("<-\"tag1\"\"tag2\"/>%md5%.%ext%", "tag2/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); + assertPath("<-\"tag1\"-\"tag2\"/>%md5%.%ext%", "/1bc29b36f623ba82aaf6724fd3b16718.jpg", "", false); } void FilenameTest::testExpandTokenSimple() { - assertExpand("image - <%artist% some text> %md5%.%ext%", - "image - %artist% some text %md5%.%ext%"); + assertPath("image - <%artist% some text> %md5%.%ext%", + "image - artist1 some text 1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTokenInvert() { - assertExpand("image - %md5%.%ext%", - "image - text %md5%.%ext%"); + assertPath("image - %md5%.%ext%", + "image - text 1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandTokenComplex() { - /*assertExpand("image - <%artist% some text <%nothing% another text> test><<%character% some text> text %nothing%> %md5%.%ext%", - "image - %artist% some text test %md5%.%ext%"); - assertExpand("image - <%model% some text <%nothing% another text> test><<%character% some text> text %nothing%> %md5%.%ext%", - "image - %md5%.%ext%");*/ + /*assertPath("image - <%artist% some text <%nothing% another text> test><<%character% some text> text %nothing%> %md5%.%ext%", + "image - artist1 some text test 1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("image - <%model% some text <%nothing% another text> test><<%character% some text> text %nothing%> %md5%.%ext%", + "image - 1bc29b36f623ba82aaf6724fd3b16718.jpg");*/ } void FilenameTest::testExpandMultipleMixed() { - assertExpand("<\"tag1\" %artist%/>%md5%.%ext%", "tag1 %artist%/%md5%.%ext%"); - assertExpand("<\"tag1\" %nothing%/>%md5%.%ext%", "%md5%.%ext%"); + assertPath("<\"tag1\" %artist%/>%md5%.%ext%", "tag1 artist1/1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag1\" %nothing%/>%md5%.%ext%", "1bc29b36f623ba82aaf6724fd3b16718.jpg"); - assertExpand("<\"tag1\"!%nothing%/>%md5%.%ext%", "tag1/%md5%.%ext%"); - assertExpand("<\"tag1\"!%artist%/>%md5%.%ext%", "%md5%.%ext%"); + assertPath("<\"tag1\"!%nothing%/>%md5%.%ext%", "tag1/1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<\"tag1\"!%artist%/>%md5%.%ext%", "1bc29b36f623ba82aaf6724fd3b16718.jpg"); - assertExpand("<\"tag1\"-%artist%/>%md5%.%ext%", "tag1/%md5%.%ext%"); - assertExpand("<-\"tag1\"%artist%/>%md5%.%ext%", "%artist%/%md5%.%ext%"); + assertPath("<\"tag1\"-%artist%/>%md5%.%ext%", "tag1/1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("<-\"tag1\"%artist%/>%md5%.%ext%", "artist1/1bc29b36f623ba82aaf6724fd3b16718.jpg"); } void FilenameTest::testExpandEscaping() { - assertExpand("<>%md5%<>", - "%md5%"); + assertPath("<>%md5%<>", "1bc29b36f623ba82aaf6724fd3b16718", "", false); } void FilenameTest::testPathOptionMax() @@ -348,7 +362,7 @@ void FilenameTest::testPathOptionTagSeparator() } void FilenameTest::testPathOptionTagSeparatorEscape() { - assertPath("%general:separator=\\,%", "tag1,tag2,tag3,test_tag1,test_tag2,test_tag3"); + assertPath("%general:separator=^,%", "tag1,tag2,tag3,test_tag1,test_tag2,test_tag3"); } void FilenameTest::testPathOptionCount() { @@ -379,18 +393,37 @@ void FilenameTest::testPathOptionNumMultiple() QFile::remove("tests/resources/tmp/7331 (1).jpg"); QFile::remove("tests/resources/tmp/7331 (2).jpg"); } +void FilenameTest::testPathOptionNumNoExt() +{ + QFile("tests/resources/image_1x1.png").copy("tests/resources/tmp/7331 (1).jpg"); + QFile("tests/resources/image_1x1.png").copy("tests/resources/tmp/7331 (2).png"); + QFile("tests/resources/image_1x1.png").copy("tests/resources/tmp/7331 (3).png"); + + assertPath("%id% (%num%).%ext%", + "7331 (2).jpg", + "tests/resources/tmp/"); + + assertPath("%id% (%num:noext%).%ext%", + "7331 (4).jpg", + "tests/resources/tmp/"); + + QFile::remove("tests/resources/tmp/7331 (1).png"); + QFile::remove("tests/resources/tmp/7331 (2).png"); +} void FilenameTest::testPathOptionNumAboveTen() { int count = 15; - for (int i = 1; i < count; ++i) + for (int i = 1; i < count; ++i) { QFile("tests/resources/image_1x1.png").copy("tests/resources/tmp/7331 (" + QString::number(i) + ").jpg"); + } assertPath("%id% (%num%).%ext%", "7331 (" + QString::number(count) + ").jpg", "tests/resources/tmp/"); - for (int i = 1; i < count; ++i) + for (int i = 1; i < count; ++i) { QFile::remove("tests/resources/tmp/7331 (" + QString::number(i) + ").jpg"); + } } void FilenameTest::testPathOptionSort() @@ -449,13 +482,19 @@ void FilenameTest::testPathForbiddenSeparator() assertPath("%copyright:separator=/%", "copyright1/copyright2"); } +void FilenameTest::testPathGalleryName() +{ + assertPath("%gallery.name%/%name%-%md5%.%ext%", "Test gallery name/-1bc29b36f623ba82aaf6724fd3b16718.jpg"); + assertPath("javascript:gallery.name + '/' + name + '-' + md5 + '.' + ext", "Test gallery name/-1bc29b36f623ba82aaf6724fd3b16718.jpg"); +} + void FilenameTest::testExpandTokensSimple() { QString format = "%artist%/%copyright%/%character%/%md5%.%ext%"; Filename fn(format); m_settings->setValue("Save/character_multiple", "replaceAll"); - QList> replaces = fn.expandTokens(format, m_img->tokens(m_profile), m_settings); + QList> replaces = fn.expandTokens(m_img->tokens(m_profile), m_settings); QCOMPARE(replaces.count(), 1); QCOMPARE(replaces[0]["artist"].toString(), QString("artist1")); @@ -468,7 +507,7 @@ void FilenameTest::testExpandTokensMultiple() Filename fn(format); m_settings->setValue("Save/character_multiple", "multiple"); - QList> replaces = fn.expandTokens(format, m_img->tokens(m_profile), m_settings); + QList> replaces = fn.expandTokens(m_img->tokens(m_profile), m_settings); QCOMPARE(replaces.count(), 2); QCOMPARE(replaces[0]["artist"].toString(), QString("artist1")); @@ -485,7 +524,7 @@ void FilenameTest::testExpandTokensMatrix() Filename fn(format); m_settings->setValue("Save/character_multiple", "multiple"); m_settings->setValue("Save/copyright_multiple", "multiple"); - QList> replaces = fn.expandTokens(format, m_img->tokens(m_profile), m_settings); + QList> replaces = fn.expandTokens(m_img->tokens(m_profile), m_settings); QCOMPARE(replaces.count(), 4); QCOMPARE(replaces[0]["artist"].toString(), QString("artist1")); @@ -516,6 +555,9 @@ void FilenameTest::testIsValid() QCOMPARE(Filename("%md5% %date:format=yyyy-MM-dd%.%ext%").isValid(), true); QCOMPARE(Filename("%md5% (%num%).%ext%").isValid(), true); + QCOMPARE(Filename("%gallery.id%/%md5%.%ext%").isValid(), true); + QCOMPARE(Filename("%toto.id%/%md5%.%ext%").isValid(), false); + QString out; Filename("%toto%.%ext%").isValid(m_profile, &out); QCOMPARE(out.isEmpty(), false); @@ -551,7 +593,10 @@ void FilenameTest::testConditionalsTag() m_settings->setValue("Filenames/0_cond", "tag7"); m_settings->setValue("Filenames/1_fn", "%id% %md5%.%ext%"); m_settings->setValue("Filenames/1_dir", QDir::homePath()); - m_settings->setValue("Filenames/1_cond", "character1"); + m_settings->setValue("Filenames/1_cond", ""); + m_settings->setValue("Filenames/2_fn", "%id% %md5%.%ext%"); + m_settings->setValue("Filenames/2_dir", QDir::homePath()); + m_settings->setValue("Filenames/2_cond", "character1"); assertPath("%artist%/%copyright%/%character%/%md5%.%ext%", "7331 1bc29b36f623ba82aaf6724fd3b16718.jpg"); } @@ -609,7 +654,10 @@ void FilenameTest::testConditionalsJavascript() m_settings->setValue("Filenames/0_cond", "javascript:width > 2000"); m_settings->setValue("Filenames/1_fn", "%id% %md5%.%ext%"); m_settings->setValue("Filenames/1_dir", QDir::homePath()); - m_settings->setValue("Filenames/1_cond", "javascript:width > 400"); + m_settings->setValue("Filenames/1_cond", "javascript:'"); + m_settings->setValue("Filenames/2_fn", "%id% %md5%.%ext%"); + m_settings->setValue("Filenames/2_dir", QDir::homePath()); + m_settings->setValue("Filenames/2_cond", "javascript:width > 400"); assertPath("%artist%/%copyright%/%character%/%md5%.%ext%", "7331 1bc29b36f623ba82aaf6724fd3b16718.jpg"); } @@ -644,7 +692,7 @@ void FilenameTest::testCommand() { Filename fn("curl -F \"user[name]=User\" -F \"user[password]=1234\" -F \"post[tags]=%all%\" -F \"post[rating]=%rating%\" -F \"post[file]=@%path%\" localhost:9000/post/create"); - QCOMPARE(fn.path(*m_img, m_profile, "", 0, false, false, false, false), + QCOMPARE(fn.path(*m_img, m_profile, "", 0, Filename::None), QStringList() << "curl -F \"user[name]=User\" -F \"user[password]=1234\" -F \"post[tags]=tag1 tag2 tag3 test_tag1 test_tag2 test_tag3 artist1 character1 character2 copyright1 copyright2\" -F \"post[rating]=safe\" -F \"post[file]=@%path%\" localhost:9000/post/create"); } @@ -687,11 +735,11 @@ void FilenameTest::testNeedTemporaryFile() void FilenameTest::testNeedExactTags() { - QCOMPARE(Filename("%md5%.%ext%").needExactTags(NULL), 0); + QCOMPARE(Filename("%md5%.%ext%").needExactTags(nullptr), 0); QCOMPARE(Filename("%md5%.%ext%").needExactTags(m_site), 0); - QCOMPARE(Filename("javascript:md5 + '.' + ext").needExactTags(NULL), 2); - QCOMPARE(Filename("%character% %md5%.%ext%").needExactTags(NULL), 1); - QCOMPARE(Filename("%all:includenamespace% %md5%.%ext%").needExactTags(NULL), 1); + QCOMPARE(Filename("javascript:md5 + '.' + ext").needExactTags(nullptr), 2); + QCOMPARE(Filename("%character% %md5%.%ext%").needExactTags(nullptr), 1); + QCOMPARE(Filename("%all:includenamespace% %md5%.%ext%").needExactTags(nullptr), 1); Filename filename("%filename%.%ext%"); QCOMPARE(filename.needExactTags(), 0); @@ -720,30 +768,44 @@ void FilenameTest::assertPath(const QString &format, const QString &expected, co assertPath(format, QStringList() << expected, path, shouldFixFilename, fullPath, keepInvalidTokens); } -void FilenameTest::assertPath(const QString &format, const QStringList &expected, QString path, bool shouldFixFilename, bool fullPath, bool keepInvalidTokens) +void FilenameTest::assertPath(const QString &format, const QStringList &expected, QString path, bool shouldFixFilename, bool fullPath, bool keepInvalidTokens, bool useTokens) { - if (path.isEmpty()) + if (path.isEmpty()) { path = QDir::homePath(); + } // Convert directory separators QStringList expectedNative; - expectedNative.reserve(expected.count()); - for (const QString &exp : expected) - { - expectedNative.append(QDir::toNativeSeparators(exp)); + if (shouldFixFilename) { + expectedNative.reserve(expected.count()); + for (const QString &exp : expected) { + expectedNative.append(QDir::toNativeSeparators(exp)); + } + } else { + expectedNative = expected; } - Filename fn(format); - QStringList actual = fn.path(*m_img, m_profile, path, 7, true, true, shouldFixFilename, fullPath, keepInvalidTokens); - QCOMPARE(actual, expectedNative); -} + Filename::PathFlags flags = Filename::Complex | Filename::CapLength; + if (shouldFixFilename) { + flags |= Filename::Fix; + } + if (fullPath) { + flags |= Filename::IncludeFolder; + } + if (keepInvalidTokens) { + flags |= Filename::KeepInvalidTokens; + } + QMap tokens; + if (useTokens) { + tokens = m_img->tokens(m_profile); + } else { + tokens.insert("allos", m_img->tokens(m_profile).value("allos")); + } -void FilenameTest::assertExpand(const QString &format, const QString &expected) -{ Filename fn(format); - QString actual = fn.expandConditionals(format, m_img->tagsString(), m_img->tokens(m_profile), m_settings); - QCOMPARE(actual, expected); + QStringList actual = fn.path(useTokens ? m_img->tokens(m_profile) : QMap(), m_profile, path, 7, flags); + QCOMPARE(actual, expectedNative); } diff --git a/tests/src/models/filename-test.h b/tests/src/models/filename-test.h index b517d1c4e..a2e4ee57e 100644 --- a/tests/src/models/filename-test.h +++ b/tests/src/models/filename-test.h @@ -29,6 +29,7 @@ class FilenameTest : public TestSuite void testPathKeepAll(); void testPathKeepN(); void testPathKeepNThenAdd(); + void testPathSort(); void testPathIgnoredTags(); void testPathEmptyDirs(); void testPathEmptyDirsNetworkDrive(); @@ -62,6 +63,7 @@ class FilenameTest : public TestSuite void testPathOptionNumSingle(); void testPathOptionNumSingleLength(); void testPathOptionNumMultiple(); + void testPathOptionNumNoExt(); void testPathOptionNumAboveTen(); void testPathOptionSort(); void testPathSpecies(); @@ -69,6 +71,7 @@ class FilenameTest : public TestSuite void testPathNoJpeg(); void testPathKeepInvalidTokens(); void testPathForbiddenSeparator(); + void testPathGalleryName(); void testExpandTokensSimple(); void testExpandTokensMultiple(); void testExpandTokensMatrix(); @@ -90,14 +93,14 @@ class FilenameTest : public TestSuite protected: void assertPath(const QString &format, const QString &expected, const QString &path = "", bool shouldFixFilename = true, bool fullPath = false, bool keepInvalidTokens = false); - void assertPath(const QString &format, const QStringList &expected, QString path = "", bool shouldFixFilename = true, bool fullPath = false, bool keepInvalidTokens = false); - void assertExpand(const QString &format, const QString &expected); + void assertPath(const QString &format, const QStringList &expected, QString path = "", bool shouldFixFilename = true, bool fullPath = false, bool keepInvalidTokens = false, bool useTokens = true); private: Profile *m_profile; QSettings *m_settings; Source *m_source; Site *m_site; + Image *m_gallery; Image *m_img; QMap m_details; }; diff --git a/tests/src/models/filtering/meta-filter-test.cpp b/tests/src/models/filtering/meta-filter-test.cpp index de35822a3..3886f366e 100644 --- a/tests/src/models/filtering/meta-filter-test.cpp +++ b/tests/src/models/filtering/meta-filter-test.cpp @@ -145,8 +145,11 @@ void MetaFilterTest::testMatchAge() tokens.insert("TESTS_now", Token(QDateTime(QDate(2016, 10, 16)))); // Basic + QCOMPARE(MetaFilter("age", ">=2seconds").match(tokens), QString()); + QCOMPARE(MetaFilter("age", ">=2mi").match(tokens), QString()); QCOMPARE(MetaFilter("age", ">=2hours").match(tokens), QString()); QCOMPARE(MetaFilter("age", ">1day").match(tokens), QString()); + QCOMPARE(MetaFilter("age", ">1w").match(tokens), QString()); QCOMPARE(MetaFilter("age", ">1mo").match(tokens), QString()); QCOMPARE(MetaFilter("age", ">=1y").match(tokens), QString("image's age does not match")); QCOMPARE(MetaFilter("age", "<1year").match(tokens), QString()); diff --git a/tests/src/models/filtering/post-filter-test.cpp b/tests/src/models/filtering/post-filter-test.cpp index 5d3125ddd..2570ce336 100644 --- a/tests/src/models/filtering/post-filter-test.cpp +++ b/tests/src/models/filtering/post-filter-test.cpp @@ -13,8 +13,9 @@ void PostFilterTest::init() { // Make tmp dir if not already existing QDir tmp("tests/resources/"); - if (!tmp.exists("tmp")) + if (!tmp.exists("tmp")) { tmp.mkdir("tmp"); + } QFile::remove("tests/resources/md5s.txt"); @@ -65,6 +66,12 @@ void PostFilterTest::cleanup() } +void PostFilterTest::testCount() +{ + QCOMPARE(PostFilter(QStringList() << "id:<=10000" << "width:>100" << "date:<2017-01-01").count(), 3); + QCOMPARE(PostFilter(QStringList() << "" << "id:<=10000").count(), 1); +} + void PostFilterTest::testFilterNumeric() { auto tokens = m_img->tokens(m_profile); diff --git a/tests/src/models/filtering/post-filter-test.h b/tests/src/models/filtering/post-filter-test.h index b32a51b7a..dd4ec6dc3 100644 --- a/tests/src/models/filtering/post-filter-test.h +++ b/tests/src/models/filtering/post-filter-test.h @@ -20,13 +20,13 @@ class PostFilterTest : public TestSuite void init(); void cleanup(); + void testCount(); void testFilterNumeric(); void testFilterSpecial(); void testFilterInvert(); private: Profile *m_profile; - QSettings *m_settings; Source *m_source; Site *m_site; Image *m_img; diff --git a/tests/src/models/image-size-test.cpp b/tests/src/models/image-size-test.cpp new file mode 100644 index 000000000..2411bb3a5 --- /dev/null +++ b/tests/src/models/image-size-test.cpp @@ -0,0 +1,131 @@ +#include "image-size-test.h" +#include +#include "models/image-size.h" + + +void ImageSizeTest::testTemporaryPath() +{ + QFile file1("tests/resources/tmp/tmp1.txt"); + QVERIFY(file1.open(QFile::Truncate | QFile::WriteOnly | QFile::Text)); + file1.write("test"); + file1.close(); + + QFile file2("tests/resources/tmp/tmp2.txt"); + QVERIFY(file2.open(QFile::Truncate | QFile::WriteOnly | QFile::Text)); + file2.write("test"); + file2.close(); + + auto *is = new ImageSize(); + + QVERIFY(is->setTemporaryPath(file1.fileName())); + QVERIFY(!is->setTemporaryPath(file1.fileName())); + QCOMPARE(is->fileSize, 4); + + QVERIFY(is->setTemporaryPath(file2.fileName())); + QVERIFY(!file1.exists()); + + delete is; + QVERIFY(!file2.exists()); +} + +void ImageSizeTest::testSavePath() +{ + QTemporaryFile file; + QVERIFY(file.open()); + file.write("test"); + file.close(); + + ImageSize is; + QVERIFY(is.setSavePath(file.fileName())); + QVERIFY(!is.setSavePath(file.fileName())); + QCOMPARE(is.fileSize, 4); +} + +void ImageSizeTest::testSaveDefault() +{ + const QString dest = "tests/resources/tmp/image-size.jpg"; + + ImageSize is; + QCOMPARE(is.save(dest), QString()); + QVERIFY(!QFile::exists(dest)); +} + +void ImageSizeTest::testSaveMove() +{ + return; // FIXME + + const QString dest = "tests/resources/tmp/image-size.jpg"; + + QTemporaryFile file; + QVERIFY(file.open()); + file.write("test"); + file.close(); + + ImageSize is; + QVERIFY(is.setTemporaryPath(file.fileName())); + QCOMPARE(is.save(dest), file.fileName()); + + QVERIFY(!file.exists()); + QVERIFY(QFile::exists(dest)); + QVERIFY(QFile::remove(dest)); +} + +void ImageSizeTest::testSaveCopy() +{ + const QString dest = "tests/resources/tmp/image-size.jpg"; + + QTemporaryFile file; + QVERIFY(file.open()); + file.write("test"); + file.close(); + + ImageSize is; + QVERIFY(is.setSavePath(file.fileName())); + QCOMPARE(is.save(dest), file.fileName()); + + QVERIFY(file.exists()); + QVERIFY(QFile::exists(dest)); + QVERIFY(QFile::remove(dest)); +} + +void ImageSizeTest::testPixmap() +{ + QPixmap pix("tests/resources/image_1x1.png"); + + ImageSize is; + is.setPixmap(pix); + + QCOMPARE(is.pixmap(), pix); +} + +void ImageSizeTest::testPixmapRect() +{ + QPixmap pix("tests/resources/image_200x200.png"); + + ImageSize is; + is.rect = QRect(0, 0, 20, 40); + is.setPixmap(pix); + + QCOMPARE(is.pixmap().size(), QSize(20, 40)); +} + +void ImageSizeTest::testSerialization() +{ + ImageSize original; + original.fileSize = 123456; + original.size = QSize(800, 600); + original.rect = QRect(10, 20, 30, 40); + + QJsonObject json; + original.write(json); + + ImageSize dest; + dest.read(json); + + QCOMPARE(dest.fileSize, original.fileSize); + QCOMPARE(dest.size, original.size); + QCOMPARE(dest.rect, original.rect); +} + + +QTEST_MAIN(ImageSizeTest) diff --git a/tests/src/models/image-size-test.h b/tests/src/models/image-size-test.h new file mode 100644 index 000000000..78b5f323e --- /dev/null +++ b/tests/src/models/image-size-test.h @@ -0,0 +1,22 @@ +#ifndef IMAGE_SIZE_TEST_H +#define IMAGE_SIZE_TEST_H + +#include "test-suite.h" + + +class ImageSizeTest : public TestSuite +{ + Q_OBJECT + + private slots: + void testTemporaryPath(); + void testSavePath(); + void testSaveDefault(); + void testSaveMove(); + void testSaveCopy(); + void testPixmap(); + void testPixmapRect(); + void testSerialization(); +}; + +#endif // IMAGE_SIZE_TEST_H diff --git a/tests/src/models/image-test.cpp b/tests/src/models/image-test.cpp index 188c8f148..60457edb0 100644 --- a/tests/src/models/image-test.cpp +++ b/tests/src/models/image-test.cpp @@ -12,8 +12,9 @@ void ImageTest::init() { // Make tmp dir if not already existing QDir tmp("tests/resources/"); - if (!tmp.exists("tmp")) + if (!tmp.exists("tmp")) { tmp.mkdir("tmp"); + } QFile::remove("tests/resources/md5s.txt"); @@ -105,7 +106,6 @@ void ImageTest::testCopy() QCOMPARE(clone.tokens(m_profile), m_img->tokens(m_profile)); QCOMPARE(clone.parentSite(), m_img->parentSite()); QCOMPARE(clone.page(), m_img->page()); - QCOMPARE(clone.data(), m_img->data()); } void ImageTest::testHasTag() @@ -116,29 +116,8 @@ void ImageTest::testHasTag() QCOMPARE(m_img->hasTag("tag7"), false); QCOMPARE(m_img->hasTag("copyright3"), false); } -void ImageTest::testHasAnyTag() -{ - QCOMPARE(m_img->hasAnyTag(QStringList() << "tag1" << "tag2"), true); - QCOMPARE(m_img->hasAnyTag(QStringList() << "tag7" << "tag1"), true); - QCOMPARE(m_img->hasAnyTag(QStringList() << "tag4" << "tag7"), false); -} -void ImageTest::testHasAllTags() -{ - QCOMPARE(m_img->hasAllTags(QStringList() << "tag1" << "tag2"), true); - QCOMPARE(m_img->hasAllTags(QStringList() << "tag7" << "tag1"), false); - QCOMPARE(m_img->hasAllTags(QStringList() << "tag4" << "tag7"), false); -} -void ImageTest::testMd5FromData() -{ - m_details.remove("md5"); - m_img->deleteLater(); - m_img = new Image(m_site, m_details, m_profile); - m_img->setData(QString("test").toLatin1()); - - QCOMPARE(m_img->md5(), QString("098f6bcd4621d373cade4e832627b4f6")); -} /*void ImageTest::testMd5FromFile() { m_details.remove("md5"); @@ -239,10 +218,11 @@ void ImageTest::testSave() { // Delete already existing QFile file("tests/resources/tmp/7331.jpg"); - if (file.exists()) + if (file.exists()) { file.remove(); + } - m_img->setData(QString("test").toLatin1()); + m_img->setSavePath("tests/resources/image_1x1.png"); QMap res = m_img->save(QString("%id%.%ext%"), QString("tests/resources/tmp/")); QCOMPARE(res.count(), 1); @@ -255,7 +235,7 @@ void ImageTest::testSaveError() { QString path = "Z:/../tests/resources/tmp/"; - m_img->setData(QString("test").toLatin1()); + m_img->setSavePath("tests/resources/image_1x1.png"); QMap res = m_img->save(QString("%id%.%ext%"), path); QCOMPARE(res.count(), 1); @@ -266,10 +246,11 @@ void ImageTest::testSaveAlreadyExists() { // Create file if not exists QFile file("tests/resources/tmp/7331.jpg"); - if (!file.open(QFile::Truncate | QFile::WriteOnly)) + if (!file.open(QFile::Truncate | QFile::WriteOnly)) { QFAIL("Cannot create file"); + } - m_img->setData(QString("test").toLatin1()); + m_img->setSavePath("tests/resources/image_1x1.png"); QMap res = m_img->save(QString("%id%.%ext%"), QString("tests/resources/tmp/")); QCOMPARE(res.count(), 1); @@ -279,10 +260,11 @@ void ImageTest::testSaveDuplicate() { // Delete already existing QFile file("tests/resources/tmp/7331.jpg"); - if (file.exists()) + if (file.exists()) { file.remove(); + } - m_img->setData(QString("test").toLatin1()); + m_img->setSavePath("tests/resources/image_1x1.png"); QMap res; QFile("tests/resources/image_1x1.png").copy("tests/resources/tmp/source.png"); @@ -315,17 +297,19 @@ void ImageTest::testSaveLog() { // Delete already existing QFile file("tests/resources/tmp/7331.jpg"); - if (file.exists()) + if (file.exists()) { file.remove(); + } QFile logFile("tests/resources/tmp/savelog.txt"); - if (logFile.exists()) + if (logFile.exists()) { logFile.remove(); + } m_settings->setValue("LogFiles/0/locationType", 1); m_settings->setValue("LogFiles/0/uniquePath", logFile.fileName()); m_settings->setValue("LogFiles/0/content", "id: %id%"); - m_img->setData(QString("test").toLatin1()); + m_img->setSavePath("tests/resources/image_1x1.png"); QMap res = m_img->save(QString("%id%.%ext%"), QString("tests/resources/tmp/")); QCOMPARE(res.count(), 1); diff --git a/tests/src/models/image-test.h b/tests/src/models/image-test.h index acea6e1cc..38664ce5a 100644 --- a/tests/src/models/image-test.h +++ b/tests/src/models/image-test.h @@ -23,9 +23,6 @@ class ImageTest : public TestSuite void testConstructor(); void testCopy(); void testHasTag(); - void testHasAnyTag(); - void testHasAllTags(); - void testMd5FromData(); // void testMd5FromFile(); void testValue(); void testLoadDetails(); diff --git a/tests/src/models/md5-database-test.cpp b/tests/src/models/md5-database-test.cpp new file mode 100644 index 000000000..9a187fec4 --- /dev/null +++ b/tests/src/models/md5-database-test.cpp @@ -0,0 +1,180 @@ +#include "md5-database-test.h" +#include +#include +#include +#include "models/md5-database.h" + + +void Md5DatabaseTest::init() +{ + QFile f("tests/resources/md5s.txt"); + f.open(QFile::WriteOnly | QFile::Text); + f.write(QString("5a105e8b9d40e1329780d62ea2265d8atests/resources/image_1x1.png\r\n").toUtf8()); + f.write(QString("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png\r\n").toUtf8()); + f.close(); + + m_settings = new QSettings("tests/resources/settings.ini", QSettings::IniFormat); +} + +void Md5DatabaseTest::cleanup() +{ + QFile::remove("tests/resources/md5s.txt"); + + m_settings->deleteLater(); +} + + +void Md5DatabaseTest::testLoad() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + QCOMPARE(md5s.exists("5a105e8b9d40e1329780d62ea2265d8a"), QString("tests/resources/image_1x1.png")); + QCOMPARE(md5s.exists("ad0234829205b9033196ba818f7a872b"), QString("tests/resources/image_1x1.png")); +} + +void Md5DatabaseTest::testAddSync() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + md5s.add("8ad8757baa8564dc136c1e07507f4a98", "tests/resources/image_1x1.png"); + QCOMPARE(md5s.exists("8ad8757baa8564dc136c1e07507f4a98"), QString("tests/resources/image_1x1.png")); + + md5s.sync(); + + QFile f("tests/resources/md5s.txt"); + f.open(QFile::ReadOnly | QFile::Text); + QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); + f.close(); + + QCOMPARE(lines.count(), 3); + QVERIFY(lines.contains("5a105e8b9d40e1329780d62ea2265d8atests/resources/image_1x1.png")); + QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); + QVERIFY(lines.contains("8ad8757baa8564dc136c1e07507f4a98tests/resources/image_1x1.png")); +} + +void Md5DatabaseTest::testAddFlush() +{ + m_settings->setValue("md5_flush_interval", 100); + + Md5Database md5s("tests/resources/md5s.txt", m_settings); + QSignalSpy spy(&md5s, SIGNAL(flushed())); + md5s.add("8ad8757baa8564dc136c1e07507f4a98", "tests/resources/image_1x1.png"); + QCOMPARE(md5s.exists("8ad8757baa8564dc136c1e07507f4a98"), QString("tests/resources/image_1x1.png")); + QVERIFY(spy.wait()); + + QFile f("tests/resources/md5s.txt"); + f.open(QFile::ReadOnly | QFile::Text); + QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); + f.close(); + + QCOMPARE(lines.count(), 3); + QVERIFY(lines.contains("5a105e8b9d40e1329780d62ea2265d8atests/resources/image_1x1.png")); + QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); + QVERIFY(lines.contains("8ad8757baa8564dc136c1e07507f4a98tests/resources/image_1x1.png")); + + m_settings->remove("md5_flush_interval"); +} + +void Md5DatabaseTest::testAddFlushOnlyOnce() +{ + m_settings->setValue("md5_flush_interval", 100); + + Md5Database md5s("tests/resources/md5s.txt", m_settings); + QSignalSpy spy(&md5s, SIGNAL(flushed())); + md5s.add("8ad8757baa8564dc136c1e07507f4a98", "tests/resources/image_1x1.png"); + md5s.add("8ad8757baa8564dc136c1e07507f4a99", "tests/resources/image_1x1.png"); + QVERIFY(spy.wait()); + QVERIFY(!spy.wait(500)); + + QCOMPARE(spy.count(), 1); + + m_settings->remove("md5_flush_interval"); +} + +void Md5DatabaseTest::testUpdate() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + md5s.set("5a105e8b9d40e1329780d62ea2265d8a", "newpath.png"); + md5s.sync(); + + QFile f("tests/resources/md5s.txt"); + f.open(QFile::ReadOnly | QFile::Text); + QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); + f.close(); + + QCOMPARE(lines.count(), 2); + QVERIFY(lines.contains("5a105e8b9d40e1329780d62ea2265d8anewpath.png")); + QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); +} + +void Md5DatabaseTest::testRemove() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + md5s.remove("5a105e8b9d40e1329780d62ea2265d8a"); + QVERIFY(md5s.exists("5a105e8b9d40e1329780d62ea2265d8a").isEmpty()); + + md5s.sync(); + + QFile f("tests/resources/md5s.txt"); + f.open(QFile::ReadOnly | QFile::Text); + QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); + f.close(); + + QCOMPARE(lines.count(), 1); + QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); +} + + +void Md5DatabaseTest::testActionDontKeepDeleted() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + m_settings->setValue("Save/md5Duplicates", "move"); + m_settings->setValue("Save/keepDeletedMd5", false); + + QPair action; + + action = md5s.action("new"); + QCOMPARE(action.first, QString("move")); + QCOMPARE(action.second, QString("")); + + md5s.add("new", "tests/resources/image_1x1.png"); + + action = md5s.action("new"); + QCOMPARE(action.first, QString("move")); + QCOMPARE(action.second, QString("tests/resources/image_1x1.png")); + + md5s.remove("new"); + + action = md5s.action("new"); + QCOMPARE(action.first, QString("move")); + QCOMPARE(action.second, QString("")); + + // Restore state + m_settings->setValue("Save/md5Duplicates", "save"); +} + +void Md5DatabaseTest::testActionKeepDeleted() +{ + Md5Database md5s("tests/resources/md5s.txt", m_settings); + m_settings->setValue("Save/md5Duplicates", "move"); + m_settings->setValue("Save/keepDeletedMd5", true); + + QPair action; + + action = md5s.action("new"); + QCOMPARE(action.first, QString("move")); + QCOMPARE(action.second, QString("")); + + md5s.add("new", "NON_EXISTING_FILE"); + + action = md5s.action("new"); + QCOMPARE(action.first, QString("ignore")); + QCOMPARE(action.second, QString("NON_EXISTING_FILE")); + + // Restore state + md5s.remove("new"); + m_settings->setValue("Save/md5Duplicates", "save"); + m_settings->setValue("Save/keepDeletedMd5", false); +} + + + +QTEST_MAIN(Md5DatabaseTest) diff --git a/tests/src/models/md5-database-test.h b/tests/src/models/md5-database-test.h new file mode 100644 index 000000000..1a808ac83 --- /dev/null +++ b/tests/src/models/md5-database-test.h @@ -0,0 +1,29 @@ +#ifndef MD5_DATABASE_TEST_H +#define MD5_DATABASE_TEST_H + +#include +#include "test-suite.h" + + +class Md5DatabaseTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testLoad(); + void testAddSync(); + void testAddFlush(); + void testAddFlushOnlyOnce(); + void testUpdate(); + void testRemove(); + void testActionDontKeepDeleted(); + void testActionKeepDeleted(); + + private: + QSettings *m_settings; +}; + +#endif // MD5_DATABASE_TEST_H diff --git a/tests/src/models/monitor-test.cpp b/tests/src/models/monitor-test.cpp new file mode 100644 index 000000000..17a4f5bd4 --- /dev/null +++ b/tests/src/models/monitor-test.cpp @@ -0,0 +1,91 @@ +#include "monitor-test.h" +#include +#include "models/monitor.h" +#include "models/profile.h" +#include "models/site.h" +#include "models/source.h" + + +void MonitorTest::init() +{ + m_profile = new Profile("tests/resources/settings.ini"); + m_source = new Source(m_profile, "release/sites/Danbooru (2.0)"); + m_site = new Site("danbooru.donmai.us", m_source); +} + +void MonitorTest::cleanup() +{ + m_profile->deleteLater(); + m_source->deleteLater(); + m_site->deleteLater(); +} + + +void MonitorTest::testSite() +{ + Monitor monitor(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + + QCOMPARE(monitor.site(), m_site); +} + +void MonitorTest::testInterval() +{ + Monitor monitor(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + + QCOMPARE(monitor.interval(), 60); +} + +void MonitorTest::testLastCheck() +{ + QDateTime before(QDate(2016, 7, 2), QTime(16, 35, 12)); + QDateTime after(QDate(2018, 7, 2), QTime(16, 35, 12)); + + Monitor monitor(m_site, 60, before, 12, true); + + QCOMPARE(monitor.lastCheck(), before); + monitor.setLastCheck(after); + QCOMPARE(monitor.lastCheck(), after); +} + +void MonitorTest::testCumulated() +{ + Monitor monitor(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + + QCOMPARE(monitor.cumulated(), 12); + QCOMPARE(monitor.preciseCumulated(), true); + monitor.setCumulated(20, false); + QCOMPARE(monitor.cumulated(), 20); + QCOMPARE(monitor.preciseCumulated(), false); +} + +void MonitorTest::testSerialization() +{ + Monitor original(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + + QJsonObject json; + original.toJson(json); + + Monitor dest = Monitor::fromJson(json, QMap {{ m_site->url(), m_site }}); + + QCOMPARE(dest.site(), original.site()); + QCOMPARE(dest.interval(), original.interval()); + QCOMPARE(dest.lastCheck(), original.lastCheck()); + QCOMPARE(dest.cumulated(), original.cumulated()); + QCOMPARE(dest.preciseCumulated(), original.preciseCumulated()); +} + +void MonitorTest::testCompare() +{ + Monitor a(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + Monitor b(m_site, 60, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + Monitor c(m_site, 120, QDateTime(QDate(2016, 7, 2), QTime(16, 35, 12)), 12, true); + + QVERIFY(a == b); + QVERIFY(b == a); + QVERIFY(a != c); + QVERIFY(b != c); + QVERIFY(c == c); +} + + +QTEST_MAIN(MonitorTest) diff --git a/tests/src/models/monitor-test.h b/tests/src/models/monitor-test.h new file mode 100644 index 000000000..7572c07b1 --- /dev/null +++ b/tests/src/models/monitor-test.h @@ -0,0 +1,32 @@ +#ifndef MONITOR_TEST_H +#define MONITOR_TEST_H + +#include "test-suite.h" + + +class Profile; +class Site; +class Source; + +class MonitorTest : public TestSuite +{ + Q_OBJECT + + private slots: + void init(); + void cleanup(); + + void testSite(); + void testInterval(); + void testLastCheck(); + void testCumulated(); + void testSerialization(); + void testCompare(); + + private: + Profile *m_profile; + Source *m_source; + Site *m_site; +}; + +#endif // MONITOR_TEST_H diff --git a/tests/src/models/page-api-test.cpp b/tests/src/models/page-api-test.cpp index 36e67d4e4..2499e31c5 100644 --- a/tests/src/models/page-api-test.cpp +++ b/tests/src/models/page-api-test.cpp @@ -19,7 +19,7 @@ void PageApiTest::init() QString path = "tests/resources/sites/Danbooru (2.0)/danbooru.donmai.us/defaults.ini"; QSettings settings(path, QSettings::IniFormat); settings.setValue("auth/pseudo", "user"); - settings.setValue("auth/password", "pass"); + settings.setValue("auth/password", "a867ce3dbb1f52ccb763d4a1ff4bee5baaea37c1"); settings.sync(); m_profile = new Profile("tests/resources/"); @@ -55,7 +55,7 @@ void PageApiTest::testParseUrlLogin() Page page(m_profile, site, m_sites, tags); PageApi pageApi(&page, m_profile, site, site->getApis().first(), tags); - QCOMPARE(pageApi.url().toString(), QString("https://danbooru.donmai.us/posts.xml?login=user&password_hash=pass&limit=25&page=1&tags=test tag")); + QCOMPARE(pageApi.url().toString(), QString("https://danbooru.donmai.us/posts.xml?limit=25&page=1&tags=test tag&login=user&password_hash=a867ce3dbb1f52ccb763d4a1ff4bee5baaea37c1")); } void PageApiTest::testParseUrlAltPage() @@ -68,7 +68,7 @@ void PageApiTest::testParseUrlAltPage() PageApi pageApi(&page, m_profile, site, site->getApis().first(), tags, 1001); pageApi.setLastPage(&prevPage); - QCOMPARE(pageApi.url().toString(), QString("https://danbooru.donmai.us/posts.xml?login=user&password_hash=pass&limit=25&page=b0&tags=test tag")); + QCOMPARE(pageApi.url().toString(), QString("https://danbooru.donmai.us/posts.xml?limit=25&page=b0&tags=test tag&login=user&password_hash=a867ce3dbb1f52ccb763d4a1ff4bee5baaea37c1")); } diff --git a/tests/src/models/profile-test.cpp b/tests/src/models/profile-test.cpp index 399017947..eafd1fa6e 100644 --- a/tests/src/models/profile-test.cpp +++ b/tests/src/models/profile-test.cpp @@ -111,112 +111,5 @@ void ProfileTest::testRemoveFavoriteThumb() } #endif -void ProfileTest::testLoadMd5s() -{ - QCOMPARE(m_profile->md5Exists("5a105e8b9d40e1329780d62ea2265d8a"), QString("tests/resources/image_1x1.png")); - QCOMPARE(m_profile->md5Exists("ad0234829205b9033196ba818f7a872b"), QString("tests/resources/image_1x1.png")); -} - -void ProfileTest::testAddMd5() -{ - m_profile->addMd5("8ad8757baa8564dc136c1e07507f4a98", "tests/resources/image_1x1.png"); - QCOMPARE(m_profile->md5Exists("8ad8757baa8564dc136c1e07507f4a98"), QString("tests/resources/image_1x1.png")); - - m_profile->sync(); - - QFile f("tests/resources/md5s.txt"); - f.open(QFile::ReadOnly | QFile::Text); - QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); - f.close(); - - QCOMPARE(lines.count(), 3); - QVERIFY(lines.contains("5a105e8b9d40e1329780d62ea2265d8atests/resources/image_1x1.png")); - QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); - QVERIFY(lines.contains("8ad8757baa8564dc136c1e07507f4a98tests/resources/image_1x1.png")); -} - -void ProfileTest::testUpdateMd5() -{ - m_profile->setMd5("5a105e8b9d40e1329780d62ea2265d8a", "newpath.png"); - m_profile->sync(); - - QFile f("tests/resources/md5s.txt"); - f.open(QFile::ReadOnly | QFile::Text); - QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); - f.close(); - - QCOMPARE(lines.count(), 2); - QVERIFY(lines.contains("5a105e8b9d40e1329780d62ea2265d8anewpath.png")); - QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); -} - -void ProfileTest::testRemoveMd5() -{ - m_profile->removeMd5("5a105e8b9d40e1329780d62ea2265d8a"); - QVERIFY(m_profile->md5Exists("5a105e8b9d40e1329780d62ea2265d8a").isEmpty()); - - m_profile->sync(); - - QFile f("tests/resources/md5s.txt"); - f.open(QFile::ReadOnly | QFile::Text); - QStringList lines = QString(f.readAll()).split("\n", QString::SkipEmptyParts); - f.close(); - - QCOMPARE(lines.count(), 1); - QVERIFY(lines.contains("ad0234829205b9033196ba818f7a872btests/resources/image_1x1.png")); -} - - -void ProfileTest::testMd5ActionDontKeepDeleted() -{ - m_profile->getSettings()->setValue("Save/md5Duplicates", "move"); - m_profile->getSettings()->setValue("Save/keepDeletedMd5", false); - - QPair action; - - action = m_profile->md5Action("new"); - QCOMPARE(action.first, QString("move")); - QCOMPARE(action.second, QString("")); - - m_profile->addMd5("new", "tests/resources/image_1x1.png"); - - action = m_profile->md5Action("new"); - QCOMPARE(action.first, QString("move")); - QCOMPARE(action.second, QString("tests/resources/image_1x1.png")); - - m_profile->removeMd5("new"); - - action = m_profile->md5Action("new"); - QCOMPARE(action.first, QString("move")); - QCOMPARE(action.second, QString("")); - - // Restore state - m_profile->getSettings()->setValue("Save/md5Duplicates", "save"); -} - -void ProfileTest::testMd5ActionKeepDeleted() -{ - m_profile->getSettings()->setValue("Save/md5Duplicates", "move"); - m_profile->getSettings()->setValue("Save/keepDeletedMd5", true); - - QPair action; - - action = m_profile->md5Action("new"); - QCOMPARE(action.first, QString("move")); - QCOMPARE(action.second, QString("")); - - m_profile->addMd5("new", "NON_EXISTING_FILE"); - - action = m_profile->md5Action("new"); - QCOMPARE(action.first, QString("ignore")); - QCOMPARE(action.second, QString("NON_EXISTING_FILE")); - - // Restore state - m_profile->removeMd5("new"); - m_profile->getSettings()->setValue("Save/md5Duplicates", "save"); - m_profile->getSettings()->setValue("Save/keepDeletedMd5", false); -} - - QTEST_MAIN(ProfileTest) diff --git a/tests/src/models/profile-test.h b/tests/src/models/profile-test.h index 6cf7ce15d..60be32e34 100644 --- a/tests/src/models/profile-test.h +++ b/tests/src/models/profile-test.h @@ -27,14 +27,6 @@ class ProfileTest : public TestSuite void testRemoveFavoriteThumb(); #endif - // MD5s - void testLoadMd5s(); - void testAddMd5(); - void testUpdateMd5(); - void testRemoveMd5(); - void testMd5ActionDontKeepDeleted(); - void testMd5ActionKeepDeleted(); - private: Profile *m_profile; QList m_dates; diff --git a/tests/src/models/site-test.cpp b/tests/src/models/site-test.cpp index d270f548a..949c7b6e9 100755 --- a/tests/src/models/site-test.cpp +++ b/tests/src/models/site-test.cpp @@ -91,36 +91,6 @@ void SiteTest::testGetSites() QCOMPARE(sites.first()->type(), QString("Danbooru (2.0)")); } -void SiteTest::testLoadTags() -{ - // Wait for tags - qRegisterMetaType>(); - QSignalSpy spy(m_site, SIGNAL(finishedLoadingTags(QList))); - m_site->loadTags(3, 20); - QVERIFY(spy.wait()); - - // Get results - QList arguments = spy.takeFirst(); - QVariantList variants = arguments.at(0).value(); - - // Convert results - QVector tags; - QStringList tagsText; - tags.reserve(variants.count()); - tagsText.reserve(variants.count()); - for (const QVariant &variant : variants) - { - Tag tag = variant.value(); - tags.append(tag); - tagsText.append(tag.text()); - } - - // Compare results - tagsText = tagsText.mid(0, 3); - QCOMPARE(tags.count(), 20); - QCOMPARE(tagsText, QStringList() << "kameji_(tyariri)" << "the_king_of_fighterx_xiv" << "condom_skirt"); -} - void SiteTest::testCookies() { QList cookies; @@ -129,8 +99,7 @@ void SiteTest::testCookies() QList cookiesVariant; cookiesVariant.reserve(cookies.count()); - for (const QNetworkCookie &cookie : cookies) - { + for (const QNetworkCookie &cookie : cookies) { cookiesVariant.append(cookie.toRawForm()); } QSettings siteSettings("tests/resources/sites/Danbooru (2.0)/danbooru.donmai.us/defaults.ini", QSettings::IniFormat); @@ -168,6 +137,9 @@ void SiteTest::testLoginNone() void SiteTest::testLoginGet() { + // FIXME: with the new auth system, you can't override auth information like this + return; + // Prepare settings QSettings siteSettings("tests/resources/sites/Danbooru (2.0)/danbooru.donmai.us/defaults.ini", QSettings::IniFormat); siteSettings.setValue("auth/pseudo", "user"); diff --git a/tests/src/models/site-test.h b/tests/src/models/site-test.h index cc9f37eb6..ea433ea33 100644 --- a/tests/src/models/site-test.h +++ b/tests/src/models/site-test.h @@ -26,7 +26,6 @@ class SiteTest : public TestSuite void testGetSites(); - void testLoadTags(); void testCookies(); void testLoginNone(); void testLoginGet(); diff --git a/tests/src/models/source-test.cpp b/tests/src/models/source-test.cpp index 126303ffd..563386c42 100644 --- a/tests/src/models/source-test.cpp +++ b/tests/src/models/source-test.cpp @@ -23,15 +23,6 @@ void SourceTest::cleanup() } -void SourceTest::testMissingXml() -{ - setupSource("Danbooru (2.0)", "tests/resources/sites/tmp/"); - QFile::remove("tests/resources/sites/tmp/model.xml"); - - Source source(m_profile, "tests/resources/sites/tmp"); - QVERIFY(source.getApis().isEmpty()); -} - void SourceTest::testMissingJavascript() { setupSource("Danbooru (2.0)", "tests/resources/sites/tmp/"); @@ -41,19 +32,6 @@ void SourceTest::testMissingJavascript() QVERIFY(source.getApis().isEmpty()); } -void SourceTest::testInvalidXml() -{ - setupSource("Danbooru (2.0)", "tests/resources/sites/tmp/"); - - QFile f("tests/resources/sites/tmp/model.xml"); - f.open(QFile::WriteOnly); - f.write(QString("test").toUtf8()); - f.close(); - - Source source(m_profile, "tests/resources/sites/tmp"); - QVERIFY(source.getApis().isEmpty()); -} - void SourceTest::testInvalidJavascript() { setupSource("Danbooru (2.0)", "tests/resources/sites/tmp/"); diff --git a/tests/src/models/source-test.h b/tests/src/models/source-test.h index b02f04e00..00b8b5d81 100644 --- a/tests/src/models/source-test.h +++ b/tests/src/models/source-test.h @@ -16,9 +16,7 @@ class SourceTest : public TestSuite void init(); void cleanup(); - void testMissingXml(); void testMissingJavascript(); - void testInvalidXml(); void testInvalidJavascript(); void testMissingSites(); void testIgnoreEmptySites(); diff --git a/tests/src/pch.cpp b/tests/src/pch.cpp deleted file mode 100644 index 2cd79060c..000000000 --- a/tests/src/pch.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "src/pch.h" diff --git a/tests/src/pch.h b/tests/src/pch.h deleted file mode 100644 index e68313ca5..000000000 --- a/tests/src/pch.h +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include - -#include -#include -#include -#include - -#include "test-suite.h" diff --git a/tests/src/search/search-format-visitor-test.cpp b/tests/src/search/search-format-visitor-test.cpp index 9ac426472..3cee36329 100644 --- a/tests/src/search/search-format-visitor-test.cpp +++ b/tests/src/search/search-format-visitor-test.cpp @@ -1,9 +1,9 @@ #include "search-format-visitor-test.h" #include -#include "search/search-format.h" -#include "search/search-format-visitor.h" #include "search/ast/search-node-op.h" #include "search/ast/search-node-tag.h" +#include "search/search-format.h" +#include "search/search-format-visitor.h" void SearchFormatVisitorTest::testOrOnly() diff --git a/tests/src/tags/tag-api-test.cpp b/tests/src/tags/tag-api-test.cpp index 16b004aeb..31fa7ae2d 100644 --- a/tests/src/tags/tag-api-test.cpp +++ b/tests/src/tags/tag-api-test.cpp @@ -21,26 +21,89 @@ void TagApiTest::cleanup() } -void TagApiTest::testBasic() +TagApi::LoadResult load(TagApi *api) { - TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); - - CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.xml"); - // Wait for downloader - QSignalSpy spy(&tagApi, SIGNAL(finishedLoading(TagApi*, TagApi::LoadResult))); - tagApi.load(false); - QVERIFY(spy.wait()); + QSignalSpy spy(api, SIGNAL(finishedLoading(TagApi*, TagApi::LoadResult))); + api->load(false); + if (!spy.wait()) { + return TagApi::LoadResult::Error; + } // Get results QList arguments = spy.takeFirst(); TagApi::LoadResult result = arguments.at(1).value(); + return result; +} + +void TagApiTest::testBasic() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.xml"); + + TagApi::LoadResult result = load(&tagApi); + QCOMPARE(result, TagApi::LoadResult::Ok); QCOMPARE(tagApi.tags().count(), 100); QCOMPARE(tagApi.tags().at(1).text(), QString("walkr")); QCOMPARE(tagApi.tags().at(1).type().name(), QString("copyright")); } +void TagApiTest::testNetworkError() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + CustomNetworkAccessManager::NextFiles.enqueue("404"); + + TagApi::LoadResult result = load(&tagApi); + + QCOMPARE(result, TagApi::LoadResult::Error); + QCOMPARE(tagApi.tags().count(), 0); +} + +void TagApiTest::testParseError() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.html"); + + TagApi::LoadResult result = load(&tagApi); + + QCOMPARE(result, TagApi::LoadResult::Error); + QCOMPARE(tagApi.tags().count(), 0); +} + +void TagApiTest::testDoubleLoad() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.xml"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.xml"); + + load(&tagApi); + TagApi::LoadResult result = load(&tagApi); + + QCOMPARE(result, TagApi::LoadResult::Ok); +} + +void TagApiTest::testRedirect() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + CustomNetworkAccessManager::NextFiles.enqueue("redirect"); + CustomNetworkAccessManager::NextFiles.enqueue("tests/resources/pages/danbooru.donmai.us/tags.xml"); + + TagApi::LoadResult result = load(&tagApi); + + QCOMPARE(result, TagApi::LoadResult::Ok); +} + +void TagApiTest::testAbort() +{ + TagApi tagApi(m_profile, m_site, m_site->getApis().first(), 1, 100); + + QSignalSpy spy(&tagApi, SIGNAL(finishedLoading(TagApi*, TagApi::LoadResult))); + tagApi.load(false); + tagApi.abort(); + QVERIFY(!spy.wait(1000)); +} + QTEST_MAIN(TagApiTest) diff --git a/tests/src/tags/tag-api-test.h b/tests/src/tags/tag-api-test.h index fe4adcb83..4bb889937 100644 --- a/tests/src/tags/tag-api-test.h +++ b/tests/src/tags/tag-api-test.h @@ -16,6 +16,11 @@ class TagApiTest : public TestSuite void cleanup(); void testBasic(); + void testNetworkError(); + void testParseError(); + void testDoubleLoad(); + void testRedirect(); + void testAbort(); private: Profile *m_profile; diff --git a/tests/src/tags/tag-database-in-memory-test.cpp b/tests/src/tags/tag-database-in-memory-test.cpp index 0b434aefc..8e5295640 100644 --- a/tests/src/tags/tag-database-in-memory-test.cpp +++ b/tests/src/tags/tag-database-in-memory-test.cpp @@ -35,11 +35,30 @@ void TagDatabaseInMemoryTest::loadEmpty() QCOMPARE(database.count(), 0); } +void TagDatabaseInMemoryTest::testLoadInvalidTypes() +{ + QTemporaryFile file; + QVERIFY(file.open()); + file.write("0,general\n1,artist\n2,invalid,test\n3,copyright\n4,character"); + file.seek(0); + + TagDatabaseInMemory database(file.fileName(), "tests/resources/tags.txt"); + QVERIFY(database.load()); + + QMap types = database.tagTypes(); + QCOMPARE(types.count(), 4); + QCOMPARE(types.keys(), QList() << 0 << 1 << 3 << 4); + QCOMPARE(types.value(0).name(), QString("general")); + QCOMPARE(types.value(1).name(), QString("artist")); + QCOMPARE(types.value(3).name(), QString("copyright")); + QCOMPARE(types.value(4).name(), QString("character")); +} + void TagDatabaseInMemoryTest::loadInvalidLines() { QTemporaryFile file; QVERIFY(file.open()); - file.write("tag1,1\ntag3\n"); + file.write("tag1,1\ntag3\ntag4,123456\n,4"); file.seek(0); TagDatabaseInMemory database("tests/resources/tag-types.txt", file.fileName()); @@ -48,8 +67,9 @@ void TagDatabaseInMemoryTest::loadInvalidLines() QMap types = database.getTagTypes(QStringList() << "tag1" << "tag3"); QCOMPARE(types.count(), 1); - QCOMPARE(types.contains("tag1"), true); - QCOMPARE(types.contains("tag3"), false); + QVERIFY(types.contains("tag1")); + QVERIFY(!types.contains("tag3")); + QVERIFY(!types.contains("tag4")); QCOMPARE(types.value("tag1").name(), QString("artist")); QCOMPARE(database.count(), 1); } @@ -67,8 +87,8 @@ void TagDatabaseInMemoryTest::loadValidData() QMap types = database.getTagTypes(QStringList() << "tag1" << "tag3"); QCOMPARE(types.count(), 2); - QCOMPARE(types.contains("tag1"), true); - QCOMPARE(types.contains("tag3"), true); + QVERIFY(types.contains("tag1")); + QVERIFY(types.contains("tag3")); QCOMPARE(types.value("tag1").name(), QString("general")); QCOMPARE(types.value("tag3").name(), QString("copyright")); QCOMPARE(database.count(), 4); diff --git a/tests/src/tags/tag-database-in-memory-test.h b/tests/src/tags/tag-database-in-memory-test.h index 893808604..e54f128de 100644 --- a/tests/src/tags/tag-database-in-memory-test.h +++ b/tests/src/tags/tag-database-in-memory-test.h @@ -14,6 +14,7 @@ class TagDatabaseInMemoryTest : public TagDatabaseTestSuite private slots: void loadNonExistingFile(); void loadEmpty(); + void testLoadInvalidTypes(); void loadInvalidLines(); void loadValidData(); void saveEmpty(); diff --git a/tests/src/tags/tag-database-test-suite.cpp b/tests/src/tags/tag-database-test-suite.cpp index 87bfabd42..c271cdff1 100644 --- a/tests/src/tags/tag-database-test-suite.cpp +++ b/tests/src/tags/tag-database-test-suite.cpp @@ -11,11 +11,19 @@ TagDatabaseTestSuite::TagDatabaseTestSuite(TagDatabase *database) void TagDatabaseTestSuite::initTestCase() { + m_database->open(); m_database->load(); m_database->setTags(QList()); } +void TagDatabaseTestSuite::testAlreadyLoaded() +{ + m_database->load(); + + QCOMPARE(m_database->tagTypes().count(), 4); +} + void TagDatabaseTestSuite::testTypesProperlyLoaded() { QMap types = m_database->tagTypes(); @@ -59,7 +67,7 @@ void TagDatabaseTestSuite::testFilledContainsAll() QCOMPARE(types.value("tag1").name(), QString("general")); QCOMPARE(types.value("tag3").name(), QString("copyright")); qDebug() << "Elapsed" << elapsed << "ms"; - QVERIFY(elapsed < 10); + QVERIFY(elapsed < 20); QCOMPARE(m_database->count(), 4); } @@ -81,5 +89,5 @@ void TagDatabaseTestSuite::testFilledContainsSome() QCOMPARE(types.value("tag1").name(), QString("general")); QCOMPARE(types.value("tag3").name(), QString("copyright")); qDebug() << "Elapsed" << elapsed << "ms"; - QVERIFY(elapsed < 10); + QVERIFY(elapsed < 20); } diff --git a/tests/src/tags/tag-database-test-suite.h b/tests/src/tags/tag-database-test-suite.h index 15452414e..09295b769 100644 --- a/tests/src/tags/tag-database-test-suite.h +++ b/tests/src/tags/tag-database-test-suite.h @@ -16,6 +16,7 @@ class TagDatabaseTestSuite : public TestSuite private slots: void initTestCase(); + void testAlreadyLoaded(); void testTypesProperlyLoaded(); void testEmptyContainsNone(); void testFilledContainsAll(); diff --git a/tests/src/tags/tag-stylist-test.cpp b/tests/src/tags/tag-stylist-test.cpp index 9a673e140..4525e3e4a 100644 --- a/tests/src/tags/tag-stylist-test.cpp +++ b/tests/src/tags/tag-stylist-test.cpp @@ -138,8 +138,9 @@ void TagStylistTest::assertSort(const QString &sort, const QStringList &expected QString format = "%1"; QStringList expected; - for (const QString &tag : expectedOrder) + for (const QString &tag : expectedOrder) { expected.append(format.arg(tag)); + } QCOMPARE(actual, expected); } diff --git a/tests/src/tags/tag-stylist-test.h b/tests/src/tags/tag-stylist-test.h index 60aa60f6a..065442c82 100644 --- a/tests/src/tags/tag-stylist-test.h +++ b/tests/src/tags/tag-stylist-test.h @@ -6,7 +6,6 @@ #include "test-suite.h" -class Profile; class QSettings; class TagStylistTest : public TestSuite @@ -32,7 +31,6 @@ class TagStylistTest : public TestSuite private: QSettings *m_settings; - Profile *m_profile; }; #endif // TAG_STYLIST_TEST_H diff --git a/tests/src/tags/tag-test.cpp b/tests/src/tags/tag-test.cpp index 19d4edbd4..2f3e3daca 100644 --- a/tests/src/tags/tag-test.cpp +++ b/tests/src/tags/tag-test.cpp @@ -228,5 +228,22 @@ void TagTest::testGetType() QCOMPARE(Tag::GetType("copyright, character", ids), QString("copyright")); } +void TagTest::testSerialization() +{ + Tag original(123, "tag", TagType("type"), 456, QStringList() << "rel 1" << "rel 2"); + + QJsonObject json; + original.write(json); + + Tag dest; + dest.read(json); + + QCOMPARE(dest.id(), original.id()); + QCOMPARE(dest.text(), original.text()); + QCOMPARE(dest.type(), original.type()); + QCOMPARE(dest.count(), original.count()); + QCOMPARE(dest.related(), original.related()); +} + QTEST_MAIN(TagTest) diff --git a/tests/src/tags/tag-test.h b/tests/src/tags/tag-test.h index b54ef0e97..1c83e8c98 100644 --- a/tests/src/tags/tag-test.h +++ b/tests/src/tags/tag-test.h @@ -4,7 +4,6 @@ #include "test-suite.h" -class Profile; class QSettings; class TagTest : public TestSuite @@ -37,10 +36,10 @@ class TagTest : public TestSuite void testSortTagsByCount(); void testTypeSpaced(); void testGetType(); + void testSerialization(); private: QSettings *m_settings; - Profile *m_profile; }; #endif // TAG_TEST_H diff --git a/tests/src/test-suite.cpp b/tests/src/test-suite.cpp index 621382828..3a8d24d3e 100755 --- a/tests/src/test-suite.cpp +++ b/tests/src/test-suite.cpp @@ -4,15 +4,13 @@ TestSuite::TestSuite() - : QObject() { getSuites().append(this); } void TestSuite::setupSource(const QString &source, QString dir) { - if (dir.isEmpty()) - { + if (dir.isEmpty()) { dir = "tests/resources/sites/"; QFile::remove(dir + "helper.js"); @@ -23,23 +21,23 @@ void TestSuite::setupSource(const QString &source, QString dir) QDir().mkpath(dir); QFile::remove(dir + "/model.js"); - QFile::remove(dir + "/model.xml"); QFile::remove(dir + "/sites.txt"); QFile("release/sites/" + source + "/model.js").copy(dir + "/model.js"); - QFile("release/sites/" + source + "/model.xml").copy(dir + "/model.xml"); QFile("release/sites/" + source + "/sites.txt").copy(dir + "/sites.txt"); } void TestSuite::setupSite(const QString &source, const QString &site, QString dir) { - if (dir.isEmpty()) - { dir = "tests/resources/sites/" + source + "/" + site; } + if (dir.isEmpty()) { + dir = "tests/resources/sites/" + source + "/" + site; + } QDir().mkpath(dir); QFile::remove(dir + "/defaults.ini"); QFile::remove(dir + "/settings.ini"); - if (QFile::exists("release/sites/" + source + "/" + site + "/defaults.ini")) - { QFile("release/sites/" + source + "/" + site + "/defaults.ini").copy(dir + "/defaults.ini"); } + if (QFile::exists("release/sites/" + source + "/" + site + "/defaults.ini")) { + QFile("release/sites/" + source + "/" + site + "/defaults.ini").copy(dir + "/defaults.ini"); + } } QList &TestSuite::getSuites() diff --git a/tests/src/updater/updater-test.cpp b/tests/src/updater/updater-test.cpp index 9554115fe..8accb9a46 100644 --- a/tests/src/updater/updater-test.cpp +++ b/tests/src/updater/updater-test.cpp @@ -17,41 +17,59 @@ void UpdaterTest::testCompareEqualAlphas() } - void UpdaterTest::testCompareMinor() { QCOMPARE(m_updater.compareVersions("1.0.1", "1.0.0"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0", "1.0.1"), -1); } void UpdaterTest::testCompareNormal() { QCOMPARE(m_updater.compareVersions("1.1.0", "1.0.0"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0", "1.1.0"), -1); } void UpdaterTest::testCompareMajor() { QCOMPARE(m_updater.compareVersions("2.0.0", "1.0.0"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0", "2.0.0"), -1); } void UpdaterTest::testCompareTen() { + QCOMPARE(m_updater.compareVersions("2.0.0", "1.10.0"), 1); QCOMPARE(m_updater.compareVersions("1.10.0", "2.0.0"), -1); } +void UpdaterTest::testCompareMissing() +{ + QCOMPARE(m_updater.compareVersions("1.0.1", "1.0"), 1); + QCOMPARE(m_updater.compareVersions("1.0", "1.0.1"), -1); +} + void UpdaterTest::testCompareAlphas() { QCOMPARE(m_updater.compareVersions("1.0.0a3", "1.0.0a2"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0a2", "1.0.0a3"), -1); } void UpdaterTest::testCompareAlphaToNew() { QCOMPARE(m_updater.compareVersions("1.0.0", "1.0.0a3"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0a3", "1.0.0"), -1); } void UpdaterTest::testCompareAlphaToOld() { QCOMPARE(m_updater.compareVersions("1.0.0a3", "0.1.0"), 1); + QCOMPARE(m_updater.compareVersions("0.1.0", "1.0.0a3"), -1); +} + +void UpdaterTest::testCompareAlphaToBeta() +{ + QCOMPARE(m_updater.compareVersions("1.0.0b1", "1.0.0a3"), 1); + QCOMPARE(m_updater.compareVersions("1.0.0a3", "1.0.0b1"), -1); } diff --git a/tests/src/updater/updater-test.h b/tests/src/updater/updater-test.h index 2738a1202..c9c69e31e 100644 --- a/tests/src/updater/updater-test.h +++ b/tests/src/updater/updater-test.h @@ -17,10 +17,12 @@ class UpdaterTest : public TestSuite void testCompareNormal(); void testCompareMajor(); void testCompareTen(); + void testCompareMissing(); void testCompareAlphas(); void testCompareAlphaToNew(); void testCompareAlphaToOld(); + void testCompareAlphaToBeta(); private: ProgramUpdater m_updater; diff --git a/uncrustify.cfg b/uncrustify.cfg index ef9287595..59b096aac 100644 --- a/uncrustify.cfg +++ b/uncrustify.cfg @@ -11,6 +11,10 @@ indent_class = 1 indent_access_spec = 1 indent_access_spec_body = true +# Force a line break at the end of files +nl_end_of_file = force +nl_end_of_file_min = 1 + # Stick pointer and reference symbol to variable align_var_def_star_style = 1 align_var_def_amp_style = 1 @@ -29,9 +33,9 @@ indent_shift = true # Keep one line control structures nl_func_leave_one_liners = true nl_cpp_lambda_leave_one_liners = true -nl_if_leave_one_liners = true -nl_while_leave_one_liners = true -nl_for_leave_one_liners = true +nl_if_leave_one_liners = false +nl_while_leave_one_liners = false +nl_for_leave_one_liners = false # Preprocessor pp_indent_at_level = true @@ -45,18 +49,26 @@ indent_switch_case = 4 cmt_indent_multi = false use_options_overriding_for_qt_macros = false +# Force braces +mod_full_brace_do = force +mod_full_brace_for = force +mod_full_brace_function = force +mod_full_brace_if = force +mod_full_brace_while = force +mod_full_brace_using = force + # Line breaks nl_enum_brace = force # "enum {" vs "enum \n {" nl_union_brace = force # "union {" vs "union \n {" nl_struct_brace = force # "struct {" vs "struct \n {" -nl_do_brace = force # "do {" vs "do \n {" -nl_if_brace = force # "if () {" vs "if () \n {" -nl_for_brace = force # "for () {" vs "for () \n {" -nl_else_brace = force # "else {" vs "else \n {" -nl_while_brace = force # "while () {" vs "while () \n {" +nl_do_brace = remove # "do {" vs "do \n {" +nl_if_brace = remove # "if () {" vs "if () \n {" +nl_for_brace = remove # "for () {" vs "for () \n {" +nl_else_brace = remove # "else {" vs "else \n {" +nl_while_brace = remove # "while () {" vs "while () \n {" nl_switch_brace = force # "switch () {" vs "switch () \n {" nl_brace_while = remove # "} while" vs "} \n while" - cuddle while -nl_brace_else = force # "} else" vs "} \n else" - cuddle else +nl_brace_else = remove # "} else" vs "} \n else" - cuddle else nl_fcall_brace = remove # "list_for_each() {" vs "list_for_each()\n{" nl_fdef_brace = force # "int foo() {" vs "int foo()\n{" @@ -66,9 +78,12 @@ sp_before_sparen = force # "if (" vs "if(" sp_after_sparen = force # "if () {" vs "if (){" sp_after_cast = remove # "(int) a" vs "(int)a" sp_cmt_cpp_start = force # '// A' vs '//A'. -sp_inside_braces = force # "{ 1 }" vs "{1}" +sp_inside_braces = ignore # "{ 1 }" vs "{1}" sp_inside_braces_struct = force # "{ 1 }" vs "{1}" sp_inside_braces_enum = force # "{ 1 }" vs "{1}" +sp_inside_braces_empty = remove # "{ }" vs "{}" +sp_else_brace = force # "else {" vs "else{" +sp_brace_else = force # "} else" vs "}else" sp_assign = force sp_arith = force sp_bool = force