diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffca6f233..6853d5e3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: # Linux Qt6 build linux-qt6: - name: Linux (Qt6) + name: Linux (Qt6 + libarchive) runs-on: ubuntu-24.04 needs: [initialization, code-format-validation] steps: @@ -84,7 +84,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libunarr-dev libgl-dev libgles2-mesa-dev \ + sudo apt-get install -y libarchive-dev libgl-dev libgles2-mesa-dev \ libfontconfig1-dev libfreetype-dev libxkbcommon-dev libpoppler-qt6-dev \ libspeechd-dev @@ -98,7 +98,6 @@ jobs: - name: Build run: | cmake -B build \ - -DDECOMPRESSION_BACKEND=unarr \ -DPDF_BACKEND=poppler \ -DBUILD_NUMBER="${{ needs.initialization.outputs.build_number }}" \ -DCMAKE_BUILD_TYPE=Release @@ -212,7 +211,7 @@ jobs: pip install -U pip pip install aqtinstall mkdir C:\Qt - python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_64 -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech + python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_64 -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech || exit /b 1 dir C:\Qt\6.9.3\msvc2022_64\bin curl.exe -L --retry 5 --retry-delay 5 --retry-all-errors "https://aka.ms/vs/17/release/vc_redist.x64.exe" -o "%GITHUB_WORKSPACE%\vc_redist.x64.exe" where iscc @@ -356,8 +355,8 @@ jobs: run: | pip install aqtinstall mkdir C:\Qt - python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_64 -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech - python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_arm64_cross_compiled -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech + python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_64 -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech || exit /b 1 + python -m aqt install-qt windows desktop 6.9.3 win64_msvc2022_arm64_cross_compiled -O c:\Qt -m qt5compat qtmultimedia qtimageformats qtshadertools qtspeech || exit /b 1 dir C:\Qt\6.9.3\msvc2022_arm64\bin curl.exe -L --retry 5 --retry-delay 5 --retry-all-errors "https://aka.ms/vs/17/release/vc_redist.arm64.exe" -o "%GITHUB_WORKSPACE%\vc_redist.arm64.exe" where iscc diff --git a/AGENTS.md b/AGENTS.md index 49b56878e..894bf8776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ cmake --build build --parallel ``` Build options: -- `DECOMPRESSION_BACKEND`: `unarr` | `7zip` | `libarchive` (default: 7zip on Windows/macOS/Linux) +- `DECOMPRESSION_BACKEND`: `unarr` | `7zip` | `libarchive` (default: 7zip on Windows/macOS, libarchive on Linux) - `PDF_BACKEND`: `pdfium` | `poppler` | `pdfkit` | `no_pdf` (default: pdfium on Windows, pdfkit on macOS, poppler on Linux) - `BUILD_SERVER_STANDALONE=ON`: builds only `YACReaderLibraryServer` (headless), requires only Qt 6.4+ - `BUILD_TESTS=ON` (default): enables the test suite diff --git a/CHANGELOG.md b/CHANGELOG.md index fccf77e71..eaded1d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ Version counting is based on semantic versioning (Major.Feature.Patch) +## 10.2.0 + +### YACReader +* Change default shortcuts for modifying the magnifying glass size to avoid conflicts with the page zoom shortcuts, `[`, `]`. +* Add an optional circular magnifying glass, with an optional ring drawn around it. The configured size is kept as a rectangle, so switching back and forth doesn't lose it. +* Add optional edge easing for the magnifying glass. The magnified region is pushed toward the edges of the view, so content near the border can be inspected without pushing the cursor all the way into the corner. +* Require a full wheel notch before the magnifying glass changes size or zoom, so a light trackpad gesture no longer resizes it. +* Fix showing the go to flow bar asking for permission to control the computer on macOS. Moving the cursor into the bar now works without granting any accessibility permission, where before it was silently doing nothing. +* Add a setting to control what the Escape key does. It can keep quitting the reader, as before, or instead cancel the topmost active mode: magnifying glass, dictionary, go to flow and then fullscreen. +* Fix crash caused by changing reading direction while quickly turning pages. +* Use pinch and ctrl+wheel mouse to change the zoom level. + +### YACReaderLibrary +* Add a library repair function to restore missing covers and rescan files that previously failed to be added. +* Add actions to create and restore library database backups. +* Automatically create and retain database backups in `.yacreaderlibrary/backups`. +* When database corruption is detected, offer to attempt a repair or restore a backup. Damaged originals are preserved in `.yacreaderlibrary/recovery`. +* Fix comics info export/import and covers package import/export error handling. +* Fix grid comic cells info height so the title doesn't overlap the other fields. +* Add a function to open the library root location. +* Add a help dialog for the search engine. Use the drop down menu in the search field. +* Add quick search presets. Use the drop down menu in the search field. +* Fix `open containing folder` asking for permission to control Finder on macOS. +* Improve the server dialog UI a bit and add the option to open the Web UI from it. + +### YACReaderLibraryServer +* Add the `repair-library` command to restore missing covers and rescan files that previously failed to be added. +* Add the `backup-library`, `list-backups`, and `restore-library` commands. +* Automatically create and retain database backups in `.yacreaderlibrary/backups`. +* Add the `repair-library-db` command to attempt to repair a damaged database. Damaged originals are preserved in `.yacreaderlibrary/recovery`. + +### WebUI +* Add a very basic web reader. +* Add rescan xml functionality to the webui. +* Add more metadata fields to the detail view. + +### All GUI apps +* New settings dialogs. + +### All apps +* Unify comics sorting to use the string based universal number everywhere. +* Experimental support for image based comics in epub format. + ## 10.1.0 ### YACReader diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bdfe0fd6..06c9dd077 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ set(YACREADER_PDF_BACKENDS pdfium poppler pdfkit no_pdf) # --- Platform defaults --- if(UNIX AND NOT APPLE) - set(_default_decompression_backend "7zip") + set(_default_decompression_backend "libarchive") set(_default_pdf_backend "poppler") elseif(APPLE) set(_default_decompression_backend "7zip") diff --git a/INSTALL.md b/INSTALL.md index b2a20b408..7f713110f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -41,7 +41,7 @@ required qml modules (Quick, QuickControls2) are missing. ### Decompression YACReader currently supports three decompression backends: 7zip, (lib)unarr, and libarchive. YACReader -defaults to 7zip for Windows, macOS, Linux, and other OSes, but you can +defaults to 7zip for Windows and macOS, and libarchive for Linux and other OSes, but you can override this using the `DECOMPRESSION_BACKEND` option: ``` @@ -52,7 +52,7 @@ cmake -B build -DDECOMPRESSION_BACKEND=libarchive #### 7zip -[7zip](https://www.7-zip.org/) is the default decompression backend. +[7zip](https://www.7-zip.org/) is the default decompression backend for Windows and macOS. It is recommended for most builds, as it currently has better support for 7z files and supports the RAR5 format. @@ -63,16 +63,20 @@ FetchContent. No manual setup is needed. As this backend is not 100% GPL compatible (unrar license restriction), it is not recommended for installations where the license is an issue. +#### libarchive + +[libarchive](https://github.com/libarchive/libarchive) is a portable, efficient decompression backend, and the default decompression backend for Linux. + +libarchive supports a wide variety of archive formats, and supports the RAR5 format (with some limitations) without using non-free software. + +The libarchive backend is recommended for packaging, and installations where GPL compatibility is required. + #### unarr [(lib)unarr](https://github.com/selmf/unarr) is a lightweight decompression backend. As of version 1.0.1, it supports less formats than 7zip, notably missing RAR5 support and only having -limited support for 7z on git versions. However, this is rarely an issue in practice as the vast majority -of comic books use either zip or RAR4 compression, which is handled nicely by this backend. - -The unarr backend is recommended for packaging, lightweight installations and generally for all users requiring -more stability than the 7zip backend can offer. +limited support for 7z on git versions. It is recommended that packagers switch to the libarchive backend. The recommended way to use this on Linux or other *NIX is to install it as a package, but you can also do an embedded build. For more information, please consult the [README](compressed_archive/unarr/README.txt) diff --git a/VERSION b/VERSION index 3b9bddfcc..fe46e0dd3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.1.0 \ No newline at end of file +10.2.0 \ No newline at end of file diff --git a/WINDOWS_LONG_PATH_SUPPORT_PLAN.md b/WINDOWS_LONG_PATH_SUPPORT_PLAN.md new file mode 100644 index 000000000..bd4b59967 --- /dev/null +++ b/WINDOWS_LONG_PATH_SUPPORT_PLAN.md @@ -0,0 +1,473 @@ +# Windows Long Path Support Plan + +## Goal + +Support Windows paths longer than the traditional `MAX_PATH` limit for the main YACReader workflows: + +- Opening comics and folders in `YACReader`. +- Creating, updating, and browsing libraries in `YACReaderLibrary`. +- Running `YACReaderLibraryServer` against libraries stored in long paths. +- Reading archives, PDFs, covers, and SQLite library databases under long paths. +- Copying, moving, deleting, importing, exporting, and opening containing folders from the GUI. + +This should not leak Windows extended-length path prefixes into user-visible UI, saved library paths, recent files, database rows, HTTP routes, or cross-platform code unless a specific low-level boundary requires it. + +## Current Assessment + +This is not a single CMake or manifest fix. The codebase mostly uses Qt path APIs, which is good, but several edges leave Qt and can still hit `MAX_PATH` or encoding problems. + +The safest approach is: + +1. Make the Windows executables long-path aware. +2. Keep normal `QString` paths internally. +3. Add narrowly-scoped Windows path helpers for native/third-party boundaries. +4. Fix or verify every boundary where paths are passed to Win32, C libraries, SQLite, external tools, or shell/file URL APIs. +5. Add Windows regression coverage around real paths longer than 260 characters. + +## Known System Requirements + +Windows long path support generally requires both: + +- An application manifest with `longPathAware` enabled. +- The machine policy/registry setting that enables Win32 long paths (`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1`, available since Windows 10 1607). + +Even with both enabled, only the Unicode (`*W`) variants of most Win32 file APIs honor the long-path opt-in. Notable APIs that still effectively cap at `MAX_PATH` regardless of manifest+policy: + +- `ShellExecute`/`ShellExecuteEx` and most of `shell32` for shell verbs. +- Explorer `/select,` invocations (the shell process must itself decide to handle the path). +- Older `*A` ANSI APIs. +- Third-party executables linked without the long-path manifest (including the bundled 7z executable). + +Implication: a manifest fixes most of the in-process code paths automatically, but anything that hands a path to the shell or to an external process still requires either a `\\?\` prefix at the boundary, a fallback path, or graceful degradation with a clear error. The app should document the OS setting in release notes or troubleshooting docs and handle failure gracefully when the OS setting is not enabled. + +## Affected Areas Found + +### Windows Executable Manifests + +The Windows targets currently add icon resource files, but no app manifest was found. Current state: + +- [YACReader/icon.rc](YACReader/icon.rc) is one line: `IDI_ICON1 ICON DISCARDABLE "icon.ico"`. +- [YACReaderLibrary/icon.rc](YACReaderLibrary/icon.rc) is the same shape. +- [YACReaderLibraryServer/CMakeLists.txt](YACReaderLibraryServer/CMakeLists.txt) does not reference any `.rc` file at all. + +Affected targets: + +- `YACReader` in [YACReader/CMakeLists.txt](YACReader/CMakeLists.txt). +- `YACReaderLibrary` in [YACReaderLibrary/CMakeLists.txt](YACReaderLibrary/CMakeLists.txt). +- `YACReaderLibraryServer` in [YACReaderLibraryServer/CMakeLists.txt](YACReaderLibraryServer/CMakeLists.txt). + +Plan: + +- Add a single shared `yacreader.manifest` next to the existing icon resources (e.g., `cmake/windows/yacreader.manifest`) containing: + +```xml + + + + + true + + + +``` + +- Embed via the existing `.rc` files using `CREATEPROCESS_MANIFEST_RESOURCE_ID` so we do not depend on CMake's auto-manifest behavior: + +```rc +#include +IDI_ICON1 ICON DISCARDABLE "icon.ico" +CREATEPROCESS_MANIFEST_RESOURCE_ID MANIFEST "yacreader.manifest" +``` + +- Add a new `icon.rc` (or `app.rc`) for `YACReaderLibraryServer` and wire it into its `target_sources` for `WIN32`. The server can reuse the same shared `.manifest` and the same `icon.ico` if desired, or ship without an icon. +- Keep icon resources intact for the two GUI apps. +- Do not pass `MANIFESTUAC`/`requireAdministrator`-style settings; we only want the long-path opt-in (and optionally `dpiAware`, `activeCodePage=UTF-8`, but those are out of scope for this plan). + +### Archive Opening + +This is the highest-risk area because comics are normally archives. + +#### 7zip backend + +[compressed_archive/compressed_archive.cpp](compressed_archive/compressed_archive.cpp) opens archives through 7zip `CInFileStream`. + +The key boundary at [compressed_archive.cpp:135-139](compressed_archive/compressed_archive.cpp#L135-L139) is: + +```cpp +#ifdef USE_UNICODE_FSTRING + if (!fileSpec->Open((LPCTSTR)filePath.toStdWString().c_str())) +#else + if (!fileSpec->Open((LPCTSTR)filePath.toStdString().c_str())) +#endif +``` + +Verified state: + +- `USE_UNICODE_FSTRING` is referenced **only** in this one file. It is never `#define`d in any header or set by CMake. The narrow branch is what compiles today on Windows. +- The bundled lib7zip source already wraps every `CreateFileW` / `FindFirstFileW` call in [CPP/Windows/FileIO.cpp](compressed_archive/lib7zip/CPP/Windows/FileIO.cpp), [CPP/Windows/FileDir.cpp](compressed_archive/lib7zip/CPP/Windows/FileDir.cpp), and [CPP/Windows/FileFind.cpp](compressed_archive/lib7zip/CPP/Windows/FileFind.cpp) with `#ifdef Z7_LONG_PATH` retry-with-superpath blocks (e.g. `if (GetSuperPath(path, superPath, USE_MAIN_PATH)) _handle = ::CreateFileW(superPath, ...)`). `Z7_LONG_PATH` is **not** currently defined for the build either. + +Plan: + +- Define `USE_UNICODE_FSTRING` for the YACReader / YACReaderLibrary targets on Windows so the wide-string branch compiles. Add it as a target-scoped compile definition; do not change cross-platform behavior. +- Define `Z7_LONG_PATH` for the lib7zip compile on Windows so the upstream long-path retry path is enabled. This is the correct long-path mechanism for this backend; do **not** add a parallel YACReader-side `\\?\` prefix helper for the 7zip boundary, which would duplicate work that lib7zip already does correctly (and locally — at the failing `CreateFileW` call, with proper UNC handling). +- After both are enabled, run smoke tests with `.cbz`, `.cbr`, `.7z`, and `.tar` archives at absolute paths > 260 characters and at > 32 760 characters (to confirm the Win32 cap is the only remaining limit). +- Only if the upstream retry still fails for a specific call site should we add a YACReader-side prefix; in practice this is unlikely. + +Out of scope: 7zip handles archive **member names** internally. We do not need to think about long entry names (they are not Win32 paths until extracted, and we extract to memory). + +#### unarr backend + +[compressed_archive/unarr/compressed_archive.cpp:14-18](compressed_archive/unarr/compressed_archive.cpp#L14-L18) already uses `ar_open_file_w((wchar_t *)filePath.utf16())` on Windows. + +Verified state: + +- unarr is shipped on Windows as a **prebuilt DLL plus header** under [dependencies/unarr/win/](dependencies/unarr/win/). There are no `.c` sources for unarr in this tree, so we cannot audit or recompile its internal `CreateFileW`/`_wfopen` usage without rebuilding the DLL from upstream. +- This means the only practical levers we have are: + 1. Trust that unarr's wide opener honors the manifest+policy (likely, since it uses `*W` APIs). + 2. Convert the path to `\\?\` form at the `ar_open_file_w` boundary if (1) fails. + +Plan: + +- Test unarr with paths over 260 characters once the manifest is in place. +- If it fails, apply the boundary helper (see Phase 1) only to the argument passed to `ar_open_file_w`. The result of the helper must not leak back into `filePath` or any caller-visible state. +- Do not block this work on rebuilding the bundled DLL. Document the bundled unarr version in release notes so future upstream bumps are tracked. + +#### libarchive backend + +`compressed_archive/libarchive/compressed_archive.cpp` uses: + +```cpp +archive_read_open_filename(a, filename.toStdString().c_str(), 10240) +``` + +Risk: + +- Not currently supported on Windows, but this would not be safe if Windows support is added. + +Plan: + +- If libarchive becomes supported on Windows, use a wide Windows opener if available or provide custom callbacks backed by `QFile`. + +### PDF Opening + +PDFium currently opens PDFs through `QFile` and `FPDF_LoadCustomDocument`, so the file path stays in Qt: + +- `common/pdf_comic.cpp` + +Risk: + +- This is probably fine once the executable is long-path aware. +- Poppler/PDFKit paths should still be smoke-tested by platform. + +Plan: + +- Include long-path PDF files in manual or automated Windows smoke tests. + +### Library Scanning and Folder Comics + +Recursive library and folder-comic scanning mostly use `QDir`, `QFileInfo`, and `QFile`. + +Relevant paths: + +- `common/comic.cpp`: recursive discovery in `Comic::findValidComicFilesInFolder`. +- `common/comic.cpp`: folder-comic image loading through `QFile`. +- `YACReaderLibrary/library_creator.cpp`: library create/update traversal, hashing, cover extraction. + +Risk: + +- These are likely OK with a manifest and Qt support. +- Recursion uses normal paths, so do not introduce `\\?\` prefixes into stored library paths. +- `QFileInfo::canonicalPath()` and similar calls can behave differently when symlinks or inaccessible long paths are involved, so the update scanner needs test coverage. + +Plan: + +- Add tests or smoke scripts for: + - Creating a new library under a long root. + - Updating an existing library. + - Opening a folder comic under a long root. + - Computing pseudo-hashes from long archive paths. + +### SQLite Library Databases + +SQLite database paths are set through Qt SQL in [YACReaderLibrary/db/data_base_management.cpp](YACReaderLibrary/db/data_base_management.cpp): + +- `createDatabase` +- `loadDatabase` +- `loadDatabaseFromFile` + +Risk: + +- `QSQLITE` should normally handle a `QString` database name, but it should be verified with `library.ydb` stored below a long path. Internally Qt's SQLite driver opens the file with `sqlite3_open_v2`, which on Windows uses `CreateFileW` — so a manifest-aware process should already work. +- `exportComicsInfo` at [data_base_management.cpp:506-510](YACReaderLibrary/db/data_base_management.cpp#L506-L510) builds raw `ATTACH DATABASE` SQL by string-concatenating the path into the SQL text: + + ```cpp + attach.prepare("ATTACH DATABASE '" + QDir().toNativeSeparators(dest) + "' AS dest;"); + ``` + + This is broken for any path containing a single quote, regardless of length. It is a standalone correctness/injection issue — long paths are not the trigger, they just make it more likely a user hits a path the user constructed via the GUI. Treat this as a separate fix that this work happens to touch. + +Plan: + +- Smoke-test database create/load/update with long library data paths. +- Fix the `ATTACH DATABASE` site as a discrete change. Options, in order of preference: + 1. Use `QSqlQuery::addBindValue` if Qt's SQLite driver supports binding the database filename in `ATTACH DATABASE` (it does in modern SQLite via parameter substitution: `ATTACH DATABASE ? AS dest`). + 2. If binding is rejected by the driver, escape single quotes by doubling them (`path.replace('\'', "''")`) before concatenation, and add a regression test with a path containing `'`. +- Include import/export comics info in the test matrix with both a long path and a path containing `'`. + +### External 7z Package Import/Export + +`YACReaderLibrary/package_manager.cpp` launches an external 7z process for `.clc` packages: + +- `createPackage`: passes destination `.clc` path and library path. +- `extractPackage`: passes output directory and package path. + +Risk: + +- External 7z may or may not support long paths depending on the bundled executable/version and argument format. +- `-o` combines the option and path in one argument. That is valid for 7z, but long paths should be tested carefully. +- If the external tool needs extended-length paths, the prefix should be applied only to the arguments passed to 7z, not to stored app paths. + +Plan: + +- Verify bundled `utils/7zip` on Windows with long package paths, long output paths, and long library roots. +- If it fails, convert affected process arguments at the process boundary. +- Consider replacing this external-process path with the in-process 7zip library in a later cleanup, as the existing TODO suggests. + +### Copy, Move, Delete, and Trash + +Relevant paths: + +- `YACReaderLibrary/comic_files_manager.cpp` + - Drag/drop path extraction through `QUrl::toLocalFile`. + - `QDir().mkpath`. + - `QFile::copy`. + - `QFile::remove`. +- `YACReaderLibrary/comics_remover.cpp` + - `QFile::moveToTrash`. + - `QFile::remove`. + - `QDir::removeRecursively`. +- `YACReaderLibrary/library_window.cpp` + - Deleting library data through `QDir::removeRecursively`. + - Saving selected covers through `QFile::copy`. + +Risk: + +- Qt filesystem operations should be mostly OK once the process is long-path aware. +- Trash integration can be more OS/shell dependent than direct remove. +- Drag/drop `QUrl` handling needs testing with long local file URLs. + +Plan: + +- Test copy, move, delete-to-trash, fallback delete, recursive folder delete, and save-covers workflows under long paths. +- If `moveToTrash` fails with long paths, keep fallback direct delete behavior and improve user-facing error reporting. + +### Shell and File URL Integration + +Relevant paths: + +- [YACReaderLibrary/library_window.cpp:1503](YACReaderLibrary/library_window.cpp#L1503) — `QDesktopServices::openUrl(QUrl("file:///" + ...))` on the parent folder. +- [YACReaderLibrary/library_window.cpp:1639](YACReaderLibrary/library_window.cpp#L1639) — `comic.data(...).toString().remove("file:///").remove("file:")` — string-based URL stripping to recover a local path. Fragile for the same reasons manual URL construction is. +- [YACReaderLibrary/library_window.cpp:2192](YACReaderLibrary/library_window.cpp#L2192) — Linux/X11 "open containing folder" via manual `file:///`. +- [YACReaderLibrary/library_window.cpp:2224](YACReaderLibrary/library_window.cpp#L2224) — open folder via manual `file:///`. +- `library_window.cpp` also calls `ShellExecuteW` with `explorer.exe /select,"path"` for Windows "open containing folder, select file". +- [YACReaderLibrary/library_comic_opener.cpp](YACReaderLibrary/library_comic_opener.cpp) — `QProcess::startDetached` for launching `YACReader.exe`, plus third-party reader command placeholder substitution. + +Risk: + +- Manually constructing `file:///` URLs is fragile for spaces, non-ASCII, `#`, `%`, UNC paths, and very long paths. Stripping URL prefixes via `.remove(...)` has the inverse problem (it does not percent-decode). +- Explorer `/select,` is known to be sensitive: even with the long-path manifest+policy, the **Explorer process** must itself be long-path aware to honor the selected path. In practice it is not reliably so. +- `ShellExecuteW` is the wide variant but the shell verb dispatch (`open`, `explore`) routes through shell handlers that may not respect the long-path policy. +- Third-party apps may not support long paths; this is not fully controllable by YACReader. + +Plan: + +- Replace **all** manual `file:///` construction with `QUrl::fromLocalFile(path)`. This is a net code-quality win independent of long paths. +- Replace the [library_window.cpp:1639](YACReaderLibrary/library_window.cpp#L1639) string-strip pattern with `QUrl(roleString).toLocalFile()` (or change the role to expose a plain path in the first place). +- For "open containing folder, select file" on Windows, prefer the COM API `SHOpenFolderAndSelectItems` over `explorer.exe /select,`. It accepts a `PIDL` from `SHParseDisplayName`, which works with long paths when the shell namespace cooperates. If both fail, fall back to opening the parent folder with `QDesktopServices::openUrl(QUrl::fromLocalFile(parentPath))` and surface a non-fatal warning. +- Keep `QProcess::startDetached` arguments as plain `QString`s first; test the Library → Reader handoff with a long-path comic. If `YACReader.exe` cannot open the path, the issue is in `YACReader.exe` itself, not the IPC. +- Document that third-party reader support depends on the external application; do not silently rewrite arguments to `\\?\` form because most third-party readers will reject that prefix. + +### Server Path Routing + +Relevant paths: + +- `YACReaderLibrary/server/requestmapper.cpp` +- `YACReaderLibrary/server/controllers/v2/*` + +Many controllers decode a request path using: + +```cpp +QString path = QUrl::fromPercentEncoding(request.getPath()).toUtf8(); +``` + +Risk: + +- These are relative library paths, not native absolute paths, so `MAX_PATH` is not directly the issue. +- However, long relative paths and non-ASCII names need round-trip testing through HTTP percent encoding, database lookup, and final filesystem access. +- The `.toUtf8()` conversion back into a `QString` deserves a separate cleanup; it is not necessarily a long-path bug, but it is suspicious. + +Plan: + +- Add server smoke tests for long nested relative paths and non-ASCII filenames. +- Avoid native Windows extended prefixes in HTTP routes. +- Consider cleaning up decode code to keep explicit `QString` handling. + +### Logging and Console Output + +Several places convert paths for logs or console output: + +- `YACReaderLibraryServer/console_ui_library_creator.cpp` +- `YACReader/main.cpp` +- `YACReaderLibrary/main.cpp` +- `YACReaderLibraryServer/main.cpp` + +Risk: + +- These conversions should not block file access, but they can make debugging long/non-ASCII paths harder. + +Plan: + +- Leave as low priority unless logs corrupt important diagnostics. +- Prefer Qt logging of `QString` paths where practical. + +## Path-Length Asymmetry in Real Libraries + +A YACReader library on disk has a strongly asymmetric path-length distribution: + +- **Library root** (e.g. `C:\Users\me\Documents\mycomics\`) — almost always short. +- **`.yacreaderlibrary` metadata folder** lives directly under the root, so `library.ydb`, the cover cache, and DB `ATTACH` source/dest paths inherit a short prefix. Their absolute paths almost never exceed `MAX_PATH`. +- **Package destinations and shell "open folder" targets** — usually point at user-chosen locations that are also short (Desktop, Documents, library root). +- **Comic files themselves (the library leaves)** — live at deep nested paths like `C:\Users\me\Documents\mycomics\Marvel\Spider-Man\Volume 1 (1963)\The Amazing Spider-Man v1 #234 - The Final Showdown - Date Foo More Foo Blah.cbz`. **These are the paths that blow past `MAX_PATH` in real-world use.** + +Implication: a small subset of the work above — covering only the leaf-file read path — delivers most of the user-visible value. DB attach quoting, shell "select in Explorer", external 7z package import/export, and `.yacreaderlibrary` metadata I/O can stay in the broader plan but are not on the critical path for "users with long-named comics can finally open them." + +This reframes the work as two scopes: + +- **Minimal scope (Phase 0):** make leaf comic files openable end-to-end, from library scan through reader display. +- **Full scope (Phases 1–5):** harden every other path-handling boundary so corner cases (rename into long path, package import/export at long destinations, shell integration, server routes, etc.) all work too. + +The minimal scope is recommended as a separate, ship-first deliverable. + +## Proposed Implementation Phases + +### Phase 0: Minimal Leaf-File Long-Path Support (recommended ship-first scope) + +**Goal:** A library rooted at a short path can contain comic files whose absolute path exceeds 260 characters, and those comics can be scanned, opened, and read. Nothing else has to work yet. + +Changes: + +1. Embed a `longPathAware` manifest in `YACReader.exe` and `YACReaderLibrary.exe` (as described under "Windows Executable Manifests"). Skip `YACReaderLibraryServer` in this phase — it is not on the read path for the GUI workflow. +2. Define `USE_UNICODE_FSTRING` for the Windows `YACReader` and `YACReaderLibrary` targets so [compressed_archive.cpp:135](compressed_archive/compressed_archive.cpp#L135) takes the wide-string branch. +3. Define `Z7_LONG_PATH` for the lib7zip Windows compile so its existing superpath-retry logic activates. +4. Smoke-test the unarr backend with a long-path archive. If it fails, prefix only the argument passed to `ar_open_file_w` at [unarr/compressed_archive.cpp:15](compressed_archive/unarr/compressed_archive.cpp#L15) using the Phase 1 boundary helper. Do not modify any other unarr call site. +5. Smoke-test PDFium with a long-path PDF. It uses `QFile` already, so the manifest alone is expected to be sufficient; if not, the fix is contained to [common/pdf_comic.cpp](common/pdf_comic.cpp). + +What is **explicitly deferred** from Phase 0: + +- `ATTACH DATABASE` quoting fix — independent correctness bug, can ship separately at any time. +- All `file:///` URL cleanup and the [library_window.cpp:1639](YACReaderLibrary/library_window.cpp#L1639) string-strip pattern — code quality, no user-visible impact for short library roots. +- "Open containing folder, select file" via `SHOpenFolderAndSelectItems` — works fine today on short library roots; the failure mode (user clicks "open folder" on a deeply-nested comic) is rare and gracefully degrades to "folder didn't open". +- External 7z package import/export — affects `.clc` workflow only, infrequent. +- Server long-path routes — server is not used by the leaf-file GUI workflow. +- Library rename/move flows that could turn a short path into a long one — uncommon. +- Cover save / drag-out flows. +- `YACReaderLibraryServer` manifest and `.rc`. + +Phase 0 acceptance: + +- A `.cbz`, `.cbr`, and `.7z` file at an absolute path > 260 chars opens in `YACReader` from a fresh Library scan. +- The library scanner enumerates and ingests those files without skipping them. +- Reader → next/prev page works for long-path archives across both 7zip and unarr backends. +- A `.pdf` at a long path opens. +- No `\\?\` prefixes appear in the library DB rows or in any user-visible UI. +- Manifest is verifiable in the linked `.exe` via `mt.exe`. + +Estimated change footprint: 1 manifest file, 2 `.rc` edits, 2 CMake `target_compile_definitions` lines, plus the unarr boundary fallback if needed. No source edits beyond conditional compilation. + +If Phase 0 ships independently, Phases 1–5 below subsume the Phase 0 changes and add the broader hardening. The Phase 0 manifest mechanics, `USE_UNICODE_FSTRING`, and `Z7_LONG_PATH` definitions carry forward unchanged. + +### Phase 1: Manifest and Boundary Helper + +If Phase 0 has already shipped, Phase 1 only needs to **extend** the manifest coverage; the helper work is the new piece. + +- Add the `longPathAware` manifest (see "Windows Executable Manifests" above for exact CMake/`.rc` mechanics) to the targets not already covered. After Phase 0 this means adding a `.rc` for `YACReaderLibraryServer` and wiring it into its `target_sources` for `WIN32`. +- If Phase 0 was skipped, embed the manifest for all three executables now (`YACReader`, `YACReaderLibrary`, `YACReaderLibraryServer`). +- Verify the manifest is actually present in each linked binary (`mt.exe -inputresource:YACReader.exe;#1 -out:check.manifest`, or open in a resource viewer). CMake's auto-manifest generation can silently override an embedded manifest in some toolchains; the explicit `CREATEPROCESS_MANIFEST_RESOURCE_ID` form avoids that. +- Add a Windows-only helper for native boundary conversion, scoped narrowly: + - Normal absolute drive path: `C:\...` → `\\?\C:\...` + - UNC path: `\\server\share\...` → `\\?\UNC\server\share\...` + - Already-prefixed path: unchanged + - Relative paths, URLs, Qt resources: rejected with a clear assertion in debug, returned unchanged in release +- Forbid the helper from being used by callers that store, log, or display the result. Document this in the helper's header. +- The helper is a **fallback**, not a default. Most boundaries (Qt file APIs, lib7zip with `Z7_LONG_PATH`, unarr's wide opener) are expected to work without it once the manifest is in place. Use only where verification proves it is needed. + +### Phase 2: Archive Backend Fixes + +If Phase 0 has shipped, steps 1–4 are already done; this phase reduces to step 5 (libarchive guard) plus deeper test coverage. If Phase 0 was skipped, run the full sequence: + +1. Define `USE_UNICODE_FSTRING` for the Windows YACReader and YACReaderLibrary targets (target-scoped compile definition; not a global header `#define`). This is the prerequisite for everything else in this phase. +2. Define `Z7_LONG_PATH` for the lib7zip compile on Windows. This activates the upstream long-path retry logic that already exists in `compressed_archive/lib7zip/CPP/Windows/`. +3. Smoke-test the 7zip backend with long-path archives. Expect this to be sufficient. +4. Smoke-test the unarr backend with long-path archives. If it fails, apply the Phase 1 boundary helper at the `ar_open_file_w` call only. +5. Leave libarchive Windows support explicitly unsupported. Add a CMake-level guard or comment at [compressed_archive/libarchive/compressed_archive.cpp:54](compressed_archive/libarchive/compressed_archive.cpp#L54) noting that the `archive_read_open_filename` call uses narrow strings and would need a wide opener / custom `QFile` callbacks before Windows enablement. + +### Phase 3: Shell, Process, and URL Cleanup + +The biggest risk in this phase is **external 7z** (see "External 7z Package Import/Export" above) — it is the most likely path-handling site that will fail and need a non-trivial workaround. Address it first so any downstream design choices (e.g., switching to in-process 7zip) can be made early. + +- Verify external 7z package import/export with long paths. If it fails, decide between: (a) prefixing only the arguments passed to the external process, (b) creating a junction/symlink to a short path as a temporary working dir, (c) accelerating the existing TODO to switch to in-process 7zip. +- Replace **all** manual local file URL construction in `YACReaderLibrary` with `QUrl::fromLocalFile`. +- Replace the `.remove("file:///").remove("file:")` pattern at [library_window.cpp:1639](YACReaderLibrary/library_window.cpp#L1639) with `QUrl(...).toLocalFile()` — or, better, change the upstream `ComicModel::CoverPathRole` to return a plain path. +- Verify Library-to-Reader launch with long paths. +- Fix Windows "open containing folder, select file" using `SHOpenFolderAndSelectItems` with `SHParseDisplayName`. Add a fallback that opens the parent folder via `QDesktopServices::openUrl(QUrl::fromLocalFile(parentPath))` if selection fails. + +### Phase 4: Database and Library Workflows + +- Fix `ATTACH DATABASE` quoting in [data_base_management.cpp:506-510](YACReaderLibrary/db/data_base_management.cpp#L506-L510) (this is a standalone correctness fix, not just a long-path fix). Try parameter binding first; fall back to `''` escaping with a regression test. +- Verify SQLite create/load/update/import/export under long paths. +- Test library create/update/delete paths and rename/move flows (including renaming a comic so its DB row's path becomes long). +- Test cover save/copy paths. + +### Phase 5: Tests and CI + +Add Windows-only tests or smoke scripts that create a temporary directory tree whose absolute path exceeds 260 characters. + +Suggested test cases: + +- `QFile`, `QDir`, and `QFileInfo` can create, enumerate, and remove long paths. +- `CompressedArchive` opens and extracts a page from a long-path `.cbz`, `.cbr`, `.7z`, and `.tar`. +- `InitialComicInfoExtractor` extracts a cover from a long-path archive. +- `PdfiumComic` opens a long-path PDF. +- `LibraryCreator` creates and updates a library rooted in a long path. +- `DataBaseManagement` creates and loads `library.ydb` in a long path. +- `DataBaseManagement` exports/imports `comics-info` to and from paths containing a single quote (`'`). +- Library-to-reader handoff opens a long-path comic. +- Library rename / move of a comic from a short path into a long path. +- Package export/import works with long source and destination paths. +- Server routes can address a comic with a long nested relative path and with non-ASCII filenames. +- Delete/trash/copy/move workflows behave correctly or report a useful error. +- Manifest verification: a built `YACReader.exe` reports `longPathAware = true` via `mt.exe`. + +CI consideration: the GitHub Actions Windows runners default to `LongPathsEnabled = 0`. The test job must either set it (admin registry write) or skip long-path tests with a clear message. Document the choice. + +## Acceptance Criteria + +- All three Windows executables are manifest long-path aware (verified via `mt.exe` or equivalent on the linked binary). +- A comic archive at an absolute path longer than 260 characters opens in `YACReader`. +- `YACReaderLibrary` can create, update, browse, and open comics from a library rooted at a long path. +- Covers and `library.ydb` are created under long `.yacreaderlibrary` paths. +- The default Windows archive backend (`7zip`) works with long paths. +- The unarr backend works with long paths or has a documented, enforced boundary fix. +- `comics-info` import/export works against paths containing a single quote (`'`) — regardless of length. +- Manual `file:///` URL construction for local paths is removed from touched code; `.remove("file:///")` patterns are removed. +- **No `\\?\` extended-length prefixes appear in stored library paths, recent-files lists, database rows, HTTP routes, settings, logs, or anywhere user-visible.** Prefixes exist only inside narrowly-scoped boundary helpers and are stripped before return. +- Failure modes are clear when Windows policy is not enabled or a third-party app/tool cannot handle long paths. + +## Open Questions + +- Which bundled 7z executable/version is shipped on Windows under [utils/7zip](utils/7zip), and does it support long path arguments? If it does not, do we accept (a) prefixing args at the boundary, (b) a short-path junction shim, or (c) accelerating the in-process 7zip migration? +- ~~Does the in-tree 7zip `CInFileStream` path type currently compile as Unicode on Windows?~~ **Resolved:** No. `USE_UNICODE_FSTRING` is undefined; the narrow branch compiles. Phase 2 fixes this. +- Should `YACReaderLibraryServer` on Windows be treated as a first-class long-path target, or only verified opportunistically? (Phase 1 includes adding a `.rc` for it, which assumes first-class.) +- Do we want automated Windows CI coverage for long paths (requires admin registry write on the runner), or a documented manual release checklist first? +- The bundled unarr is a prebuilt DLL — do we want to track its upstream version in release notes / `dependencies/unarr/README` so future bumps are auditable? +- Should the `ATTACH DATABASE` escaping fix ship as a separate PR before this work, since it is a standalone correctness bug? diff --git a/YACReader/configuration.cpp b/YACReader/configuration.cpp index a424aef7b..e4df16791 100644 --- a/YACReader/configuration.cpp +++ b/YACReader/configuration.cpp @@ -28,6 +28,12 @@ void Configuration::load(QSettings *settings) settings->setValue(MAG_GLASS_SIZE, QSize(350, 175)); if (!settings->contains(MAG_GLASS_ZOOM)) settings->setValue(MAG_GLASS_ZOOM, 0.5); + if (!settings->contains(MAG_GLASS_CIRCULAR)) + settings->setValue(MAG_GLASS_CIRCULAR, false); + if (!settings->contains(MAG_GLASS_RING)) + settings->setValue(MAG_GLASS_RING, true); + if (!settings->contains(MAG_GLASS_EDGE_EASE)) + settings->setValue(MAG_GLASS_EDGE_EASE, true); if (!settings->contains(FLOW_TYPE)) settings->setValue(FLOW_TYPE, 0); if (!settings->contains(FULLSCREEN)) diff --git a/YACReader/configuration.h b/YACReader/configuration.h index d872cd3e4..74f4a58e3 100644 --- a/YACReader/configuration.h +++ b/YACReader/configuration.h @@ -32,6 +32,11 @@ enum MouseMode { HotAreas }; +enum EscapeKeyBehavior { + EscapeQuits = 0, + EscapeCancelsMode = 1 +}; + class Configuration : public QObject { Q_OBJECT @@ -55,6 +60,12 @@ class Configuration : public QObject void setMagnifyingGlassSize(const QSize &mgs) { settings->setValue(MAG_GLASS_SIZE, mgs); } float getMagnifyingGlassZoom() { return settings->value(MAG_GLASS_ZOOM, 0.5).toFloat(); } void setMagnifyingGlassZoom(float mgz) { settings->setValue(MAG_GLASS_ZOOM, mgz); } + bool getMagnifyingGlassCircular() { return settings->value(MAG_GLASS_CIRCULAR, false).toBool(); } + void setMagnifyingGlassCircular(bool circular) { settings->setValue(MAG_GLASS_CIRCULAR, circular); } + bool getMagnifyingGlassRing() { return settings->value(MAG_GLASS_RING, true).toBool(); } + void setMagnifyingGlassRing(bool ring) { settings->setValue(MAG_GLASS_RING, ring); } + bool getMagnifyingGlassEdgeEase() { return settings->value(MAG_GLASS_EDGE_EASE, true).toBool(); } + void setMagnifyingGlassEdgeEase(bool ease) { settings->setValue(MAG_GLASS_EDGE_EASE, ease); } QSize getGotoSlideSize() { return settings->value(GO_TO_FLOW_SIZE).toSize(); } void setGotoSlideSize(const QSize &gss) { settings->setValue(GO_TO_FLOW_SIZE, gss); } float getZoomLevel() { return settings->value(ZOOM_LEVEL).toFloat(); } @@ -117,6 +128,9 @@ class Configuration : public QObject MouseMode getMouseMode() { return static_cast(settings->value(MOUSE_MODE, MouseMode::Normal).toInt()); } void setMouseMode(MouseMode mouseMode) { settings->setValue(MOUSE_MODE, static_cast(mouseMode)); } + EscapeKeyBehavior getEscapeKeyBehavior() { return static_cast(settings->value(ESCAPE_KEY_BEHAVIOR, EscapeKeyBehavior::EscapeQuits).toInt()); } + void setEscapeKeyBehavior(EscapeKeyBehavior behavior) { settings->setValue(ESCAPE_KEY_BEHAVIOR, static_cast(behavior)); } + ScaleMethod getScalingMethod() { return static_cast(settings->value(SCALING_METHOD, static_cast(ScaleMethod::Lanczos)).toInt()); } void setScalingMethod(ScaleMethod method) { settings->setValue(SCALING_METHOD, static_cast(method)); } }; diff --git a/YACReader/magnifying_glass.cpp b/YACReader/magnifying_glass.cpp index 306c48a8f..dd58bbe72 100644 --- a/YACReader/magnifying_glass.cpp +++ b/YACReader/magnifying_glass.cpp @@ -2,24 +2,118 @@ #include "viewer.h" -MagnifyingGlass::MagnifyingGlass(int w, int h, float zoomLevel, QWidget *parent) - : QLabel(parent), zoomLevel(zoomLevel) +#include +#include + +MagnifyingGlass::MagnifyingGlass(int w, int h, float zoomLevel, bool circular, bool ring, QWidget *parent) + : QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring) { setup(QSize(w, h)); } -MagnifyingGlass::MagnifyingGlass(const QSize &size, float zoomLevel, QWidget *parent) - : QLabel(parent), zoomLevel(zoomLevel) +MagnifyingGlass::MagnifyingGlass(const QSize &size, float zoomLevel, bool circular, bool ring, QWidget *parent) + : QLabel(parent), zoomLevel(zoomLevel), circular(circular), ring(ring) { setup(size); } void MagnifyingGlass::setup(const QSize &size) { - resize(size); + logicalSize = size; + resize(displaySize()); setScaledContents(true); setMouseTracking(true); setCursor(QCursor(QBitmap(1, 1), QBitmap(1, 1))); + applyShape(); +} + +QSize MagnifyingGlass::displaySize() const +{ + if (circular) { + const int side = qMax(logicalSize.width(), logicalSize.height()); + return QSize(side, side); + } + return logicalSize; +} + +void MagnifyingGlass::applyShape() +{ + if (circular) + setMask(QRegion(rect(), QRegion::Ellipse)); + else + clearMask(); +} + +void MagnifyingGlass::setCircular(bool circular) +{ + if (this->circular == circular) + return; + this->circular = circular; + // Only the display geometry and mask change; logicalSize (and thus the saved + // MAG_GLASS_SIZE) must not be touched, so do not emit sizeChanged here. + resize(displaySize()); + applyShape(); + updateImage(); +} + +void MagnifyingGlass::setRing(bool ring) +{ + if (this->ring == ring) + return; + this->ring = ring; + if (circular) + update(); // ring only affects the circular rendering; repaint, no geometry change +} + +void MagnifyingGlass::paintEvent(QPaintEvent *event) +{ + if (!circular) { + QLabel::paintEvent(event); + return; + } + + const QPixmap pm = pixmap(); + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + const QRectF fullRect(rect()); + + if (!ring) { + QPainterPath clip; + clip.addEllipse(fullRect); + painter.setClipPath(clip); + if (!pm.isNull()) + painter.drawPixmap(rect(), pm); // mirrors setScaledContents: scale to fill + return; + } + + // Circular + ring. The widget mask (setMask) is a hard-edged ellipse, so anything + // drawn out to the widget boundary keeps that aliased silhouette. Instead, inset the + // whole loupe a couple of pixels inside the mask and let the bezel's own antialiased + // outer edge be the silhouette: the thin margin between bezel and mask stays unpainted + // (transparent) so the page shows through and the antialiased edge blends into it. + const qreal bezelWidth = qMax(2.0, width() / 80.0); + const qreal outerInset = 1.5; // transparent margin left for the antialiased blend + const QRectF outerRect = fullRect.adjusted(outerInset, outerInset, -outerInset, -outerInset); + const QRectF innerRect = outerRect.adjusted(bezelWidth, bezelWidth, -bezelWidth, -bezelWidth); + + // Content clipped to just past the bezel's inner edge, so the content's own (hard) + // clip edge is hidden underneath the opaque part of the bezel. + QPainterPath contentClip; + contentClip.addEllipse(innerRect.adjusted(-0.5, -0.5, 0.5, 0.5)); + painter.setClipPath(contentClip); + if (!pm.isNull()) + painter.drawPixmap(rect(), pm); + painter.setClipping(false); + + // Bezel as a filled annulus so both edges are antialiased: the inner edge blends onto + // the content, the outer edge blends onto the page. + QPainterPath bezel; + bezel.setFillRule(Qt::OddEvenFill); + bezel.addEllipse(outerRect); + bezel.addEllipse(innerRect); + painter.fillPath(bezel, QColor(30, 30, 30)); } void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event) @@ -31,7 +125,12 @@ void MagnifyingGlass::mouseMoveEvent(QMouseEvent *event) void MagnifyingGlass::updateImage(int x, int y) { auto *const viewer = qobject_cast(parentWidget()); - QImage img = viewer->grabMagnifiedRegion(QPoint(x, y), size(), zoomLevel); + // The loupe widget follows the cursor (and may overhang the window edge, as before). Its + // *content* is sampled at the eased center, so the zoomed image swims a little toward the + // edges within the loupe — bounded by the loupe's own half-size so the cursor's point + // never leaves the view. + const QPoint sampleCenter = viewer->easeViewerPos(QPoint(x, y), size(), circular); + QImage img = viewer->grabMagnifiedRegion(sampleCenter, size(), zoomLevel); setPixmap(QPixmap::fromImage(img)); move(static_cast(x - float(width()) / 2), static_cast(y - float(height()) / 2)); } @@ -46,38 +145,67 @@ void MagnifyingGlass::updateImage() } void MagnifyingGlass::wheelEvent(QWheelEvent *event) { - switch (event->modifiers()) { - // size - case Qt::NoModifier: - if (event->angleDelta().y() < 0) - sizeUp(); - else - sizeDown(); - break; - // size height - case Qt::ControlModifier: - if (event->angleDelta().y() < 0) - heightUp(); - else - heightDown(); - break; - // size width - case Qt::AltModifier: // alt modifier can actually modify the behavior of the event delta, so let's check both x & y - if (event->angleDelta().y() < 0 || event->angleDelta().x() < 0) - widthUp(); - else - widthDown(); - break; - // zoom level - case Qt::ShiftModifier: - if (event->angleDelta().y() < 0) - zoomIn(); - else - zoomOut(); - break; - default: - break; // Never propagate a wheel event to the parent widget, even if we ignore it. + // One notch of a real mouse wheel is 120 angle-delta units in a single event, so this + // threshold makes a mouse still step once per notch while a trackpad's tiny events must + // sum to 120 before stepping — the "intent" that stops a faint brush from resizing. + static constexpr int scrollStepThreshold = 120; + // Drop a partial accumulation that has gone stale, so an old half-finished gesture can't + // leak into an unrelated later one. + static constexpr qint64 scrollResetMs = 400; + + const Qt::KeyboardModifiers modifiers = event->modifiers(); + + // The active gesture reads a single signed axis. Alt (width) can swap the delta onto the + // x axis, so for it take whichever axis carries the larger movement. + int delta = 0; + if (modifiers == Qt::AltModifier) { + const int dy = event->angleDelta().y(); + const int dx = event->angleDelta().x(); + delta = (qAbs(dx) > qAbs(dy)) ? dx : dy; + } else { + delta = event->angleDelta().y(); + } + + // Only the four handled gestures accumulate; anything else is swallowed (never propagated + // to the parent) without touching the accumulator. + const bool handled = modifiers == Qt::NoModifier || modifiers == Qt::ControlModifier || modifiers == Qt::AltModifier || modifiers == Qt::ShiftModifier; + if (!handled || delta == 0) { + event->setAccepted(true); + return; + } + + // Reset the running total when the gesture changes (different modifier) or when too much + // time has passed since the last wheel event of this gesture. + if (modifiers != lastScrollModifiers || !scrollTimer.isValid() || scrollTimer.elapsed() > scrollResetMs) + scrollAccumulator = 0; + lastScrollModifiers = modifiers; + scrollTimer.restart(); + + scrollAccumulator += delta; + + // A fast, high-magnitude event may cross the threshold several times over; step once per + // crossing and keep the remainder so accumulation stays smooth. + while (qAbs(scrollAccumulator) >= scrollStepThreshold) { + const bool up = scrollAccumulator < 0; // convention: negative delta grows the loupe + switch (modifiers) { + case Qt::NoModifier: + up ? sizeUp() : sizeDown(); + break; + case Qt::ControlModifier: + up ? heightUp() : heightDown(); + break; + case Qt::AltModifier: + up ? widthUp() : widthDown(); + break; + case Qt::ShiftModifier: + up ? zoomIn() : zoomOut(); + break; + default: + break; + } + scrollAccumulator -= up ? -scrollStepThreshold : scrollStepThreshold; } + event->setAccepted(true); } void MagnifyingGlass::zoomIn() @@ -100,46 +228,46 @@ void MagnifyingGlass::zoomOut() void MagnifyingGlass::sizeUp() { - auto w = width(); - auto h = height(); + auto w = logicalSize.width(); + auto h = logicalSize.height(); if (growWidth(w) | growHeight(h)) // bitwise OR prevents short-circuiting resizeAndUpdate(w, h); } void MagnifyingGlass::sizeDown() { - auto w = width(); - auto h = height(); + auto w = logicalSize.width(); + auto h = logicalSize.height(); if (shrinkWidth(w) | shrinkHeight(h)) // bitwise OR prevents short-circuiting resizeAndUpdate(w, h); } void MagnifyingGlass::heightUp() { - auto h = height(); + auto h = logicalSize.height(); if (growHeight(h)) - resizeAndUpdate(width(), h); + resizeAndUpdate(logicalSize.width(), h); } void MagnifyingGlass::heightDown() { - auto h = height(); + auto h = logicalSize.height(); if (shrinkHeight(h)) - resizeAndUpdate(width(), h); + resizeAndUpdate(logicalSize.width(), h); } void MagnifyingGlass::widthUp() { - auto w = width(); + auto w = logicalSize.width(); if (growWidth(w)) - resizeAndUpdate(w, height()); + resizeAndUpdate(w, logicalSize.height()); } void MagnifyingGlass::widthDown() { - auto w = width(); + auto w = logicalSize.width(); if (shrinkWidth(w)) - resizeAndUpdate(w, height()); + resizeAndUpdate(w, logicalSize.height()); } void MagnifyingGlass::reset() @@ -151,8 +279,10 @@ void MagnifyingGlass::reset() void MagnifyingGlass::resizeAndUpdate(int w, int h) { - resize(w, h); - emit sizeChanged(size()); + logicalSize = QSize(w, h); + resize(displaySize()); + applyShape(); + emit sizeChanged(logicalSize); // persist the rectangle, never the circular square updateImage(); } diff --git a/YACReader/magnifying_glass.h b/YACReader/magnifying_glass.h index d174c93f0..48d9c82a1 100644 --- a/YACReader/magnifying_glass.h +++ b/YACReader/magnifying_glass.h @@ -1,8 +1,10 @@ #ifndef __MAGNIFYING_GLASS #define __MAGNIFYING_GLASS +#include #include #include +#include #include class MagnifyingGlass : public QLabel @@ -10,8 +12,35 @@ class MagnifyingGlass : public QLabel Q_OBJECT private: float zoomLevel; + // The rectangle the user configures via the size gestures. This is the source of + // truth for sizing and the only value ever persisted to MAG_GLASS_SIZE. The widget's + // actual geometry (see displaySize()) may differ from this in circular mode. + QSize logicalSize; + // When true the loupe is rendered as a circle whose diameter is the wider of the two + // logicalSize dimensions. The widget grows to a square for display, but logicalSize + // (and therefore the saved setting) is left untouched. + bool circular; + // When true (and circular), a bezel ring is drawn along the circle boundary to hide + // the aliased edge left by the circular mask. Has no effect in rectangular mode. + bool ring; + + // Wheel/scroll accumulation for the resize & zoom gestures. Rather than stepping on the + // mere sign of each wheel event (which makes a trackpad's many tiny high-resolution + // events each fire a full step), we sum the signed angle delta of the active gesture and + // only take a step once it crosses scrollStepThreshold. A real mouse wheel delivers 120 + // units per notch in a single event, so it still steps once per notch; a light trackpad + // brush no longer does anything. + int scrollAccumulator = 0; + Qt::KeyboardModifiers lastScrollModifiers = Qt::NoModifier; + QElapsedTimer scrollTimer; + void setup(const QSize &size); void resizeAndUpdate(int w, int h); + // The widget geometry to use for the current mode: a max(w, h) square when circular, + // otherwise the logical rectangle. + QSize displaySize() const; + // Masks the widget to a circle (or clears the mask) to match the current mode. + void applyShape(); // The following 4 functions increase/decrease their argument and return true, // unless the maximum dimension value has been reached, in which case they @@ -22,9 +51,10 @@ class MagnifyingGlass : public QLabel bool shrinkHeight(int &h) const; public: - MagnifyingGlass(int width, int height, float zoomLevel, QWidget *parent); - MagnifyingGlass(const QSize &size, float zoomLevel, QWidget *parent); + MagnifyingGlass(int width, int height, float zoomLevel, bool circular, bool ring, QWidget *parent); + MagnifyingGlass(const QSize &size, float zoomLevel, bool circular, bool ring, QWidget *parent); void mouseMoveEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; public slots: void updateImage(int x, int y); void updateImage(); @@ -37,6 +67,8 @@ public slots: void heightDown(); void widthUp(); void widthDown(); + void setCircular(bool circular); + void setRing(bool ring); void reset(); signals: diff --git a/YACReader/main_window_viewer.cpp b/YACReader/main_window_viewer.cpp index 0c5a2b4cf..1372cc05a 100644 --- a/YACReader/main_window_viewer.cpp +++ b/YACReader/main_window_viewer.cpp @@ -39,10 +39,6 @@ #include #include -#ifdef use_unarr -#include "unarr.h" -#endif - namespace { QString comicBaseName(const QString &path) @@ -222,6 +218,7 @@ MainWindowViewer::~MainWindowViewer() delete showShorcutsAction; delete showInfoAction; delete closeAction; + delete exitAction; delete showDictionaryAction; delete adjustToFullSizeAction; delete fitToPageAction; @@ -569,10 +566,19 @@ void MainWindowViewer::createActions() showInfoAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(SHOW_INFO_ACTION_Y)); connect(showInfoAction, &QAction::triggered, viewer, &Viewer::informationSwitch); - closeAction = new QAction(tr("Close"), this); + // closeAction owns the Escape key. Depending on the EscapeKeyBehavior setting it either + // quits (default) or cancels the topmost active mode. The File▸Close menu command lives + // on exitAction below, which always quits regardless of the setting. + closeAction = new QAction(tr("Escape"), this); + // The shortcuts editor lists actions by toolTip(), which otherwise falls back to the + // action text; name the behaviour rather than the key, which it already shows. + closeAction->setToolTip(tr("Escape key: quit, or cancel the active mode")); closeAction->setData(CLOSE_ACTION_Y); closeAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(CLOSE_ACTION_Y)); - connect(closeAction, &QAction::triggered, this, &QWidget::close); + connect(closeAction, &QAction::triggered, this, &MainWindowViewer::onEscapePressed); + + exitAction = new QAction(tr("Close"), this); + connect(exitAction, &QAction::triggered, this, &QWidget::close); showDictionaryAction = new QAction(tr("Show Dictionary"), this); // showDictionaryAction->setCheckable(true); @@ -791,7 +797,7 @@ void MainWindowViewer::createToolBars() fileMenu->addMenu(recentmenu); fileMenu->addSeparator(); - fileMenu->addAction(closeAction); + fileMenu->addAction(exitAction); auto editMenu = new QMenu(tr("Edit")); editMenu->addAction(leftRotationAction); @@ -938,13 +944,8 @@ void MainWindowViewer::updateContextMenuPolicy() void MainWindowViewer::open() { QFileDialog openDialog; -#ifndef use_unarr - QString pathFile = openDialog.getOpenFileName(this, tr("Open Comic"), currentDirectory, tr("Comic files") + "(*.cbr *.cbz *.rar *.zip *.tar *.pdf *.7z *.cb7 *.arj *.cbt)"); -#elif (UNARR_API_VERSION < 110) - QString pathFile = openDialog.getOpenFileName(this, tr("Open Comic"), currentDirectory, tr("Comic files") + "(*.cbr *.cbz *.rar *.zip *.tar *.pdf *.cbt)"); -#else - QString pathFile = openDialog.getOpenFileName(this, tr("Open Comic"), currentDirectory, tr("Comic files") + "(*.cbr *.cbz *.rar *.zip *.tar *.pdf *.cbt *.7z *.cb7)"); -#endif + const QString fileFilter = tr("Comic files") + " (" + Comic::comicExtensions.join(' ') + ')'; + QString pathFile = openDialog.getOpenFileName(this, tr("Open Comic"), currentDirectory, fileFilter); if (!pathFile.isEmpty()) { openComicFromPath(pathFile); } @@ -1196,6 +1197,45 @@ void MainWindowViewer::toggleFullScreen() Configuration::getConfiguration().setFullScreen(fullscreen = !fullscreen); } +void MainWindowViewer::onEscapePressed() +{ + if (Configuration::getConfiguration().getEscapeKeyBehavior() == EscapeCancelsMode) { + // Cancel the topmost active mode; if none is active this is a no-op (does not quit). + cancelActiveMode(); + return; + } + + close(); +} + +bool MainWindowViewer::cancelActiveMode() +{ + // This order is documented to users in the Options ▸ General ▸ "Escape key" tooltip; + // keep the two in sync when adding or reordering modes. + if (viewer->magnifyingGlassIsVisible()) { + viewer->hideMagnifyingGlass(); + showMagnifyingGlassAction->setChecked(false); + return true; + } + + if (viewer->translatorIsVisible()) { + viewer->animateHideTranslator(); + return true; + } + + if (viewer->goToFlowIsVisible()) { + viewer->animateHideGoToFlow(); + return true; + } + + if (fullscreen) { + toggleFullScreen(); + return true; + } + + return false; +} + void MainWindowViewer::toFullScreen() { fromMaximized = this->isMaximized(); @@ -1607,30 +1647,7 @@ void MainWindowViewer::getSiblingComics(QString path, QString currentComic) { QDir d(path); d.setFilter(QDir::Files | QDir::NoDotAndDotDot); -#ifndef use_unarr - d.setNameFilters(QStringList() << "*.cbr" - << "*.cbz" - << "*.rar" - << "*.zip" - << "*.tar" - << "*.pdf" - << "*.7z" - << "*.cb7" - << "*.arj" - << "*.cbt"); -#else - d.setNameFilters(QStringList() << "*.cbr" - << "*.cbz" - << "*.rar" - << "*.zip" - << "*.tar" - << "*.pdf" -#if (UNARR_API_VERSION >= 110) - << "*.7z" - << "*.cb7" -#endif - << "*.cbt"); -#endif + d.setNameFilters(Comic::comicExtensions); d.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); QStringList list = d.entryList(); std::sort(list.begin(), list.end(), naturalSortLessThanCI); @@ -1756,6 +1773,7 @@ void MainWindowViewer::applyTheme(const Theme &theme) setIcon(showShorcutsAction, toolbarTheme.showShorcutsAction, toolbarTheme.showShorcutsAction18x18); setIcon(showInfoAction, toolbarTheme.showInfoAction, toolbarTheme.showInfoAction18x18); setIcon(closeAction, toolbarTheme.closeAction, toolbarTheme.closeAction18x18); + setIcon(exitAction, toolbarTheme.closeAction, toolbarTheme.closeAction18x18); setIcon(showDictionaryAction, toolbarTheme.showDictionaryAction, toolbarTheme.showDictionaryAction18x18); setIcon(adjustToFullSizeAction, toolbarTheme.adjustToFullSizeAction, toolbarTheme.adjustToFullSizeAction18x18); setIcon(fitToPageAction, toolbarTheme.fitToPageAction, toolbarTheme.fitToPageAction18x18); diff --git a/YACReader/main_window_viewer.h b/YACReader/main_window_viewer.h index c1d10caf4..d960422d6 100644 --- a/YACReader/main_window_viewer.h +++ b/YACReader/main_window_viewer.h @@ -81,6 +81,10 @@ public slots: void toggleFitToWidthSlider(); + // Escape key handling: either quits (default) or cancels the topmost active mode, + // depending on the EscapeKeyBehavior setting. + void onEscapePressed(); + /*void viewComic(); void prev(); void next(); @@ -135,7 +139,12 @@ public slots: QAction *leftRotationAction; QAction *rightRotationAction; QAction *showInfoAction; - QAction *closeAction; + QAction *closeAction; // owns the Escape key; dispatches quit vs. cancel-mode + QAction *exitAction; // File▸Close menu command, always quits + + // Cancels the topmost active mode (magnifier, translator, go-to-flow, fullscreen). + // Returns true if a mode was cancelled, false if none was active. + bool cancelActiveMode(); QAction *doublePageAction; QAction *doubleMangaPageAction; QAction *continuousScrollAction; diff --git a/YACReader/mouse_handler.cpp b/YACReader/mouse_handler.cpp index dcdfec9f4..260eb8442 100644 --- a/YACReader/mouse_handler.cpp +++ b/YACReader/mouse_handler.cpp @@ -99,7 +99,10 @@ void YACReader::MouseHandler::mouseMoveEvent(QMouseEvent *event) auto position = event->position(); if (viewer->magnifyingGlassShown) - viewer->mglass->move(static_cast(position.x() - float(viewer->mglass->width()) / 2), static_cast(position.y() - float(viewer->mglass->height()) / 2)); + // Route through updateImage so the loupe uses the same eased content center and + // on-screen-clamped widget position as its own mouseMoveEvent handler, instead of + // snapping to the raw cursor (which fought the easing near the edges). + viewer->mglass->updateImage(static_cast(position.x()), static_cast(position.y())); if (viewer->render->hasLoadedComic()) { if (viewer->showGoToFlowAnimation->state() != QPropertyAnimation::Running) { @@ -109,7 +112,9 @@ void YACReader::MouseHandler::mouseMoveEvent(QMouseEvent *event) if (gtfPos.y() < 0 || gtfPos.x() < 0 || gtfPos.x() > viewer->goToFlow->width()) // TODO this extra check is for Mavericks (mouseMove over goToFlowGL seems to be broken) viewer->animateHideGoToFlow(); // goToFlow->hide(); - } else { + } else if (!viewer->magnifyingGlassShown) { + // Don't pop up the bottom page-selection bar while the + // magnifying glass is active: it prevents magnifying that area. int umbral = (viewer->width() - viewer->goToFlow->width()) / 2; if ((position.y() > viewer->height() - 15) && (position.x() > umbral) && (position.x() < viewer->width() - umbral)) { diff --git a/YACReader/options_dialog.cpp b/YACReader/options_dialog.cpp index f54af77c8..e5ce25eef 100644 --- a/YACReader/options_dialog.cpp +++ b/YACReader/options_dialog.cpp @@ -6,6 +6,7 @@ #include "theme_factory.h" #include "theme_manager.h" #include "yacreader_3d_flow_config_widget.h" +#include "yacreader_settings_widget.h" #include "yacreader_spin_slider_widget.h" #include @@ -20,13 +21,12 @@ #include #include #include -#include #include OptionsDialog::OptionsDialog(QWidget *parent) : YACReaderOptionsDialog(parent) { - auto tabWidget = new QTabWidget(); + auto settingsWidget = new YACReaderSettingsWidget(); auto layout = new QVBoxLayout(this); @@ -61,6 +61,18 @@ OptionsDialog::OptionsDialog(QWidget *parent) displayLayout->addWidget(showTimeInInformationLabel); displayBox->setLayout(displayLayout); + QGroupBox *magnifyingGlassBox = new QGroupBox(tr("Magnifying glass")); + auto magnifyingGlassLayout = new QVBoxLayout(); + circularMagnifyingGlass = new QCheckBox(tr("Circular magnifying glass")); + magnifyingGlassRing = new QCheckBox(tr("Draw a ring around the circular magnifying glass")); + // The ring only applies to the circular loupe, so it is enabled only when circular is on. + connect(circularMagnifyingGlass, &QCheckBox::toggled, magnifyingGlassRing, &QWidget::setEnabled); + magnifyingGlassEdgeEase = new QCheckBox(tr("Ease cursor movement toward the edges")); + magnifyingGlassLayout->addWidget(circularMagnifyingGlass); + magnifyingGlassLayout->addWidget(magnifyingGlassRing); + magnifyingGlassLayout->addWidget(magnifyingGlassEdgeEase); + magnifyingGlassBox->setLayout(magnifyingGlassLayout); + connect(pathFindButton, &QAbstractButton::clicked, this, &OptionsDialog::findFolder); QGroupBox *slideSizeBox = new QGroupBox(tr("\"Go to flow\" size")); @@ -119,15 +131,39 @@ OptionsDialog::OptionsDialog(QWidget *parent) mouseModeBox->setLayout(mouseModeLayout); + auto escapeKeyBox = new QGroupBox(tr("Escape key")); + auto escapeKeyLayout = new QVBoxLayout(); + + escapeQuitsRadioButton = new QRadioButton(tr("Quit the reader")); + escapeCancelsModeRadioButton = new QRadioButton(tr("Cancel the active mode")); + + escapeQuitsRadioButton->setToolTip(tr("Escape closes the reader, even while a mode is active.")); + + // Keep this list in sync with MainWindowViewer::cancelActiveMode(), which implements the order. + //: Tooltip listing the order in which modes are cancelled. Only the first + //: active mode in the list is cancelled per Escape keypress. + escapeCancelsModeRadioButton->setToolTip(tr("Escape cancels the first of these that is active:\n" + "\n" + "1. Magnifying glass\n" + "2. Dictionary\n" + "3. Go to page bar\n" + "4. Fullscreen\n" + "\n" + "If none is active, Escape does nothing.")); + + escapeKeyLayout->addWidget(escapeQuitsRadioButton); + escapeKeyLayout->addWidget(escapeCancelsModeRadioButton); + + escapeKeyBox->setLayout(escapeKeyLayout); + addShortcutsSection(escapeKeyBox); + layoutGeneral->addWidget(pathBox); layoutGeneral->addWidget(languageBox); layoutGeneral->addWidget(displayBox); - layoutGeneral->addWidget(slideSizeBox); + layoutGeneral->addWidget(magnifyingGlassBox); // layoutGeneral->addWidget(fitBox); - layoutGeneral->addWidget(colorBox); layoutGeneral->addWidget(scrollBox); layoutGeneral->addWidget(mouseModeBox); - layoutGeneral->addWidget(shortcutsBox); layoutGeneral->addStretch(); // GENERAL END --------------------------------------- @@ -141,6 +177,7 @@ OptionsDialog::OptionsDialog(QWidget *parent) disableShowOnMouseOver = new QCheckBox(tr("Disable mouse over activation")); layoutFlow->addWidget(gl); + layoutFlow->addWidget(slideSizeBox); layoutFlow->addWidget(quickNavi); layoutFlow->addWidget(disableShowOnMouseOver); @@ -248,15 +285,17 @@ OptionsDialog::OptionsDialog(QWidget *parent) []() { return ThemeManager::instance().getCurrentTheme().sourceJson; }, [](const QJsonObject &json) { ThemeManager::instance().setTheme(makeTheme(json)); }, this); + pageAppearance->addSection(colorBox); // APPEARANCE END ------------------------------------ - tabWidget->addTab(pageGeneral, tr("General")); - tabWidget->addTab(pageFlow, tr("Page Flow")); - tabWidget->addTab(pageImage, tr("Image adjustment")); - tabWidget->addTab(pageAppearance, tr("Appearance")); + settingsWidget->addPage(pageGeneral, tr("General")); + settingsWidget->addPage(pageFlow, tr("Page Flow")); + settingsWidget->addPage(pageImage, tr("Image adjustment")); + settingsWidget->addPage(pageAppearance, tr("Appearance")); + settingsWidget->addPage(shortcutsPage, shortcutsPage->windowTitle()); - layout->addWidget(tabWidget); + layout->addWidget(settingsWidget); auto buttons = new QHBoxLayout(); buttons->addStretch(); @@ -271,8 +310,6 @@ OptionsDialog::OptionsDialog(QWidget *parent) setModal(true); setWindowTitle(tr("Options")); - this->layout()->setSizeConstraint(QLayout::SetFixedSize); - initTheme(this); } @@ -312,6 +349,10 @@ void OptionsDialog::saveOptions() Configuration::getConfiguration().setShowTimeInInformation(showTimeInInformationLabel->isChecked()); + Configuration::getConfiguration().setMagnifyingGlassCircular(circularMagnifyingGlass->isChecked()); + Configuration::getConfiguration().setMagnifyingGlassRing(magnifyingGlassRing->isChecked()); + Configuration::getConfiguration().setMagnifyingGlassEdgeEase(magnifyingGlassEdgeEase->isChecked()); + if (!backgroundColorFollowsTheme) { settings->setValue(BACKGROUND_COLOR, currentColor); } else { @@ -337,6 +378,9 @@ void OptionsDialog::saveOptions() } Configuration::getConfiguration().setMouseMode(mouseMode); + Configuration::getConfiguration().setEscapeKeyBehavior( + escapeCancelsModeRadioButton->isChecked() ? EscapeCancelsMode : EscapeQuits); + Configuration::getConfiguration().setScalingMethod(static_cast(scalingMethodCombo->currentIndex())); emit changedImageOptions(); @@ -365,6 +409,11 @@ void OptionsDialog::restoreOptions(QSettings *settings) showTimeInInformationLabel->setChecked(Configuration::getConfiguration().getShowTimeInInformation()); + circularMagnifyingGlass->setChecked(Configuration::getConfiguration().getMagnifyingGlassCircular()); + magnifyingGlassRing->setChecked(Configuration::getConfiguration().getMagnifyingGlassRing()); + magnifyingGlassRing->setEnabled(circularMagnifyingGlass->isChecked()); + magnifyingGlassEdgeEase->setChecked(Configuration::getConfiguration().getMagnifyingGlassEdgeEase()); + backgroundColorFollowsTheme = !settings->contains(BACKGROUND_COLOR); updateColor(backgroundColorFollowsTheme ? theme.viewer.defaultBackgroundColor @@ -409,6 +458,11 @@ void OptionsDialog::restoreOptions(QSettings *settings) hotAreasMouseModeRadioButton->setChecked(true); break; } + + if (Configuration::getConfiguration().getEscapeKeyBehavior() == EscapeCancelsMode) + escapeCancelsModeRadioButton->setChecked(true); + else + escapeQuitsRadioButton->setChecked(true); } void OptionsDialog::updateColor(const QColor &color) diff --git a/YACReader/options_dialog.h b/YACReader/options_dialog.h index eb65fe1dd..b75a8adb9 100644 --- a/YACReader/options_dialog.h +++ b/YACReader/options_dialog.h @@ -33,6 +33,10 @@ class OptionsDialog : public YACReaderOptionsDialog, protected Themable QCheckBox *showTimeInInformationLabel; + QCheckBox *circularMagnifyingGlass; + QCheckBox *magnifyingGlassRing; + QCheckBox *magnifyingGlassEdgeEase; + QCheckBox *quickNavi; QCheckBox *disableShowOnMouseOver; QCheckBox *scaleCheckbox; @@ -71,6 +75,9 @@ class OptionsDialog : public YACReaderOptionsDialog, protected Themable QRadioButton *leftRightNavigationMouseModeRadioButton; QRadioButton *hotAreasMouseModeRadioButton; + QRadioButton *escapeQuitsRadioButton; + QRadioButton *escapeCancelsModeRadioButton; + public slots: void saveOptions() override; void restoreOptions(QSettings *settings) override; diff --git a/YACReader/render.cpp b/YACReader/render.cpp index 743fe7b0b..03a4dd4b7 100644 --- a/YACReader/render.cpp +++ b/YACReader/render.cpp @@ -406,6 +406,23 @@ Render::~Render() void Render::render() { updateBuffer(); + + // The buffer and pageRenders lists rotate together. A page that was being + // prefetched can therefore become the current page while its render is + // still in flight. Do not replace (or restart) that worker: it owns the + // write to this buffer slot and must remain tracked until it finishes. + PageRender *currentPageRender = pageRenders[currentPageBufferedIndex]; + if (currentPageRender != nullptr) { + Q_ASSERT(currentPageRender->getNumPage() == currentIndex); + if (currentPageRender->isRunning() || buffer[currentPageBufferedIndex]->isNull()) { + emit processingPage(); + } else { + prepareAvailablePage(currentIndex); + } + fillBuffer(); + return; + } + if (buffer[currentPageBufferedIndex]->isNull()) { if (pagesReady.size() > 0) { if (pagesReady[currentIndex]) { @@ -940,7 +957,6 @@ void Render::rotateLeft() // Calcula el número de nuevas páginas que hay que buferear y si debe hacerlo por la izquierda o la derecha (según sea el sentido de la lectura) void Render::updateBuffer() { - QMutexLocker locker(&mutex); int windowSize = currentIndex - previousIndex; if (windowSize > 0) // add pages to right pages and remove on the left @@ -998,9 +1014,9 @@ void Render::fillBuffer() for (int i = 1; i <= qMax(numLeftPages, numRightPages); i++) { if ((currentIndex + i < (int)comic->numPages()) && + pageRenders[currentPageBufferedIndex + i] == 0 && buffer[currentPageBufferedIndex + i]->isNull() && i <= numRightPages && - pageRenders[currentPageBufferedIndex + i] == 0 && pagesReady[currentIndex + i]) // preload next pages { pageRenders[currentPageBufferedIndex + i] = new PageRender(this, currentIndex + i, comic->getRawData()->at(currentIndex + i), buffer[currentPageBufferedIndex + i], imageRotation, filters); @@ -1009,9 +1025,9 @@ void Render::fillBuffer() } if ((currentIndex - i >= 0) && + pageRenders[currentPageBufferedIndex - i] == 0 && buffer[currentPageBufferedIndex - i]->isNull() && i <= numLeftPages && - pageRenders[currentPageBufferedIndex - i] == 0 && pagesReady[currentIndex - i]) // preload previous pages { pageRenders[currentPageBufferedIndex - i] = new PageRender(this, currentIndex - i, comic->getRawData()->at(currentIndex - i), buffer[currentPageBufferedIndex - i], imageRotation, filters); diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index df7cbacc9..78370628a 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -19,9 +19,44 @@ #include #include #include +#include #include #include +#include + +#ifdef Q_OS_MACOS +#include +#endif + +namespace { +// QCursor::setPos moves the pointer by synthesizing a mouse event and injecting +// it into the HID event stream (QCocoaCursor::setPos -> CGEventPost). macOS +// gates that behind the accessibility "control this computer" permission, so on +// a machine that hasn't granted it the call is silently denied and the pointer +// never moves, while the user gets an unexplained permission request. +// +// CGWarpMouseCursorPosition repositions the pointer without injecting an event +// and needs no permission. It doesn't deliver a mouse move to the application, +// which suits the only caller here: the point is to reposition the pointer +// *without* triggering the move handling that would hide the widget again. +// +// Reported upstream as https://qt-project.atlassian.net/browse/QTBUG-148709. +// If Qt switches QCocoaCursor::setPos to the warp, this can go back to being a +// plain QCursor::setPos call once the fixed version is the minimum supported. +void moveCursorTo(const QPoint &globalPos) +{ +#ifdef Q_OS_MACOS + CGWarpMouseCursorPosition(CGPointMake(globalPos.x(), globalPos.y())); + // Warping leaves a short interval where physical mouse movement is filtered + // out; re-associating ends it so the next movement registers immediately. + CGAssociateMouseAndMouseCursorPosition(true); +#else + QCursor::setPos(globalPos); +#endif +} +} + Viewer::Viewer(QWidget *parent) : QScrollArea(parent), fullscreen(false), @@ -38,8 +73,34 @@ Viewer::Viewer(QWidget *parent) shouldOpenPrevious(false), magnifyingGlassShown(false), restoreMagnifyingGlass(false), + pinchStartZoom(100), + zoomAnchorNormX(0.5), + zoomAnchorNormY(0.5), + zoomHud(nullptr), + zoomHudHideTimer(nullptr), + zoomPreviewFinishTimer(nullptr), mouseHandler(std::make_unique(this)) { + grabGesture(Qt::PinchGesture); + + zoomHud = new QLabel(this); + zoomHud->setAlignment(Qt::AlignCenter); + zoomHud->setAttribute(Qt::WA_TransparentForMouseEvents); + zoomHud->setTextFormat(Qt::RichText); + zoomHud->setStyleSheet( + "background-color: rgba(0, 0, 0, 153); border-radius: 3px;"); + zoomHud->setFixedSize(100, 60); + zoomHud->hide(); + + zoomHudHideTimer = new QTimer(this); + zoomHudHideTimer->setSingleShot(true); + connect(zoomHudHideTimer, &QTimer::timeout, zoomHud, &QWidget::hide); + + zoomPreviewFinishTimer = new QTimer(this); + zoomPreviewFinishTimer->setSingleShot(true); + zoomPreviewFinishTimer->setInterval(250); + connect(zoomPreviewFinishTimer, &QTimer::timeout, this, &Viewer::renderFinalZoomImage); + translator = new YACReaderTranslator(this); translator->hide(); translatorAnimation = new QPropertyAnimation(translator, "pos"); @@ -78,8 +139,12 @@ Viewer::Viewer(QWidget *parent) mglass = new MagnifyingGlass( Configuration::getConfiguration().getMagnifyingGlassSize(), Configuration::getConfiguration().getMagnifyingGlassZoom(), + Configuration::getConfiguration().getMagnifyingGlassCircular(), + Configuration::getConfiguration().getMagnifyingGlassRing(), this); + magnifierEdgeEase = Configuration::getConfiguration().getMagnifyingGlassEdgeEase(); + connect(mglass, &MagnifyingGlass::sizeChanged, this, [](QSize size) { Configuration::getConfiguration().setMagnifyingGlassSize(size); }); @@ -435,6 +500,8 @@ void Viewer::updatePage() void Viewer::updateContentSize() { + cancelZoomPreview(); + // there is an image to resize if (currentPage != nullptr && !currentPage->isNull()) { QSize pagefit = currentPage->size(); @@ -764,6 +831,16 @@ void Viewer::wheelEvent(QWheelEvent *event) return; } + // Check the modifier before choosing the regular mouse/trackpad scroll path so + // high-resolution devices with pixelDelta (notably on macOS) zoom as well. Qt maps + // ControlModifier to Command on macOS unless the application opts out of that mapping. + if (event->modifiers() == Qt::ControlModifier && event->angleDelta().y() != 0) { + wheelEventZoom(event); + return; + } + + wheelZoomAccumulator = 0; + if (!event->pixelDelta().isNull()) { wheelEventTrackpad(event); } else { @@ -771,6 +848,36 @@ void Viewer::wheelEvent(QWheelEvent *event) } } +void Viewer::wheelEventZoom(QWheelEvent *event) +{ + static constexpr int wheelStep = 120; + static constexpr int zoomStep = 10; + static constexpr qint64 accumulatorResetMs = 400; + static constexpr int hudTimeoutMs = 500; + + horizontalScroller->stop(); + verticalScroller->stop(); + wheelStop = false; + + if (!wheelZoomTimer.isValid() || wheelZoomTimer.elapsed() > accumulatorResetMs) { + wheelZoomAccumulator = 0; + } + wheelZoomTimer.restart(); + + wheelZoomAccumulator += event->angleDelta().y(); + const int steps = wheelZoomAccumulator / wheelStep; + wheelZoomAccumulator -= steps * wheelStep; + + if (steps != 0) { + captureZoomAnchor(); + if (applyZoomAtAnchor(zoom + steps * zoomStep)) { + zoomHudHideTimer->start(hudTimeoutMs); + } + } + + event->accept(); +} + void Viewer::wheelEventMouse(QWheelEvent *event) { auto delta = event->angleDelta(); @@ -922,8 +1029,115 @@ QList Viewer::currentVisiblePages() return pages; } +namespace { +// The magnifier edge easing pushes the loupe's sampled center outward toward the viewport +// edges so edge content is reachable with less cursor travel. Crucially the push is bounded +// by the loupe's own half-size, which (a) keeps the cursor's true point inside the loupe +// view and (b) scales the effect with loupe size: a minimum-size loupe is nearly linear, a +// large one eases strongly. + +// Fraction of the half-axis at which the easing reaches full displacement. Smaller = the +// effect ramps in sooner and saturates before the cursor reaches the edge, so edge content +// is reachable well before the pointer is jammed against the border; beyond this the loupe +// is already at full reach (still capped at the loupe half-size, so the cursor point stays +// in view). 1.0 would only reach full push exactly at the edge. +constexpr double edgeReach = 0.4; + +// Overall strength of the push, as a fraction of the loupe half-extent (its natural cap). 1.0 +// pushes the sampled center by up to a full half-loupe at the edge; lower values keep the +// content swim gentler so it tracks the cursor more closely. The cursor's point stays in view +// for any value in (0, 1]. +constexpr double edgeStrength = 0.3; + +// Smoothstep ramp: 0 at the viewport center (1:1 there, and zero slope so it stays linear +// near the middle), rising to 1 by edgeReach (and held there to the edge). Multiplied by the +// loupe half-extent to give the outward displacement. +double edgeRamp(double u) // u = |normalized cursor offset from center|, in [0, 1] +{ + u = qBound(0.0, u / edgeReach, 1.0); + return u * u * (3.0 - 2.0 * u); +} + +// The edge easing is canceled on an axis only once the page is letterboxed by at least this +// fraction of its own size on that axis. Below it — a thin margin, an exact fit, or a page +// that overflows the viewport — the curve still applies, so the effect stays visible for +// pages that nearly fill the view instead of vanishing the instant the page is a hair smaller +// than the viewport. Above it the background beside the page is wide enough that easing would +// mostly reveal that background, so the axis is left at 1:1. +constexpr double minLetterboxFraction = 0.10; +} + +QPoint Viewer::easeViewerPos(const QPoint &viewerPos, const QSize &glassSize, bool circular) const +{ + if (!magnifierEdgeEase) { + return viewerPos; + } + // Reference frame = the viewport (the visible scroll region the page is drawn in), not the + // top-level window. The window includes the toolbar/chrome, whose height inflates the + // frame well beyond the page and pushes the "center" off, so the loupe eases too hard. In + // fullscreen the viewport already fills the screen, so this matches the old behavior there. + const double vpW = viewport()->width(); + const double vpH = viewport()->height(); + if (vpW <= 1.0 || vpH <= 1.0) { + return viewerPos; + } + + // Normalized cursor offset from the viewport center, per axis in [-1, 1]. + const double tx = qBound(-1.0, (viewerPos.x() - vpW / 2.0) / (vpW / 2.0), 1.0); + const double ty = qBound(-1.0, (viewerPos.y() - vpH / 2.0) / (vpH / 2.0), 1.0); + + // Easing helps on an axis where there is off-page content to bring toward the cursor. A page + // that overflows the viewport always qualifies; a letterboxed page qualifies until the + // margin beside it grows large enough that easing would mostly reveal background. Cancel the + // curve on an axis only once the letterbox reaches minLetterboxFraction of the page's size on + // that axis, so a page that nearly fills the viewport (thin margin or exact fit) still eases + // instead of dropping the effect the instant the page is a hair smaller than the view. + bool easeX = true; + bool easeY = true; + if (const QWidget *w = widget()) { + double pageW = w->width(); + const double pageH = w->height(); + if (continuousScroll && w == continuousWidget && continuousViewModel != nullptr) { + // The continuous widget fills the viewport width with each page centered inside it, + // so the page under the cursor — not the widget — is the horizontal extent. The + // document is contiguous vertically, so the widget height is the vertical extent. + const int cwY = viewerPos.y() + verticalScrollBar()->sliderPosition(); + const int idx = qBound(0, continuousViewModel->pageAtY(cwY), continuousViewModel->numPages() - 1); + pageW = continuousViewModel->scaledPageSize(idx).width(); + } + // Letterbox = how far the viewport exceeds the page on the axis; keep easing until it + // reaches minLetterboxFraction of the page dimension (overflow and exact fit stay on the + // "ease" side). + easeX = (vpW - pageW) < minLetterboxFraction * pageW; + easeY = (vpH - pageH) < minLetterboxFraction * pageH; + } + + // Outward displacement, capped per axis at the loupe half-extent (the loupe's "reach") and + // scaled by edgeStrength. The cap is what guarantees the cursor's point never leaves the + // loupe view, and what makes a small loupe nearly linear while a large loupe eases hard. + double dx = easeX ? edgeStrength * (glassSize.width() / 2.0) * edgeRamp(qAbs(tx)) * (tx < 0.0 ? -1.0 : 1.0) : 0.0; + double dy = easeY ? edgeStrength * (glassSize.height() / 2.0) * edgeRamp(qAbs(ty)) * (ty < 0.0 ? -1.0 : 1.0) : 0.0; + + if (circular) { + // A round loupe's limit is radial (Pythagorean): the displacement vector may not + // exceed the radius, rather than being capped independently on each axis. + const double radius = qMax(glassSize.width(), glassSize.height()) / 2.0; + const double mag = std::sqrt(dx * dx + dy * dy); + if (mag > radius && mag > 0.0) { + dx *= radius / mag; + dy *= radius / mag; + } + } + + // The displacement is a translation, so add it back in the caller's (viewport) frame. + return QPoint(qRound(viewerPos.x() + dx), qRound(viewerPos.y() + dy)); +} + QImage Viewer::grabMagnifiedRegion(const QPoint &viewerPos, const QSize &glassSize, float zoomLevel) const { + // viewerPos is expected already eased (see MagnifyingGlass::updateImage / easeViewerPos): + // this samples the loupe's *content*, which swims a little toward the edge relative to the + // loupe widget (which itself follows the cursor). const int glassW = glassSize.width(); const int glassH = glassSize.height(); const int zoomW = static_cast(glassW * zoomLevel); @@ -1070,8 +1284,12 @@ QImage Viewer::grabMagnifiedRegion(const QPoint &viewerPos, const QSize &glassSi } if (outImage) { + // The magnified image is kept at source resolution (device pixel ratio 1), + // matching the in-bounds path below. Do NOT set a >1 device pixel ratio here: + // a QPainter draws in logical coordinates, so painting the DPR-1 source crop + // onto a DPR>1 image would scale the content up by the ratio (and clip it), + // producing a sudden magnification jump at the image edges on HiDPI displays. QImage img(zoomWScaled, zoomHScaled, QImage::Format_RGB32); - img.setDevicePixelRatio(devicePixelRatioF()); img.fill(bgColor); if (zw > 0 && zh > 0) { QPainter painter(&img); @@ -1159,6 +1377,16 @@ void Viewer::translatorSwitch() translator->isVisible() ? animateHideTranslator() : animateShowTranslator(); } +bool Viewer::translatorIsVisible() const +{ + return translator->isVisible(); +} + +bool Viewer::goToFlowIsVisible() const +{ + return goToFlow->isVisible(); +} + void Viewer::showGoToFlow() { if (render->hasLoadedComic()) { @@ -1215,7 +1443,7 @@ void Viewer::moveCursoToGoToFlow() cursorX = x1 + 10; if (cursorX >= x2) cursorX = x2 - 10; - cursor().setPos(mapToGlobal(QPoint(cursorX, cursorY))); + moveCursorTo(mapToGlobal(QPoint(cursorX, cursorY))); hideCursorTimer->stop(); showCursor(); } @@ -1663,11 +1891,136 @@ bool Viewer::eventFilter(QObject *obj, QEvent *event) return QScrollArea::eventFilter(obj, event); } +bool Viewer::event(QEvent *event) +{ + if (event->type() == QEvent::Gesture) { + return gestureEvent(static_cast(event)); + } + return QScrollArea::event(event); +} + +void Viewer::captureZoomAnchor() +{ + zoomAnchorViewport = viewport()->mapFromGlobal(QCursor::pos()); + if (content->width() > 0 && content->height() > 0) { + const QPoint cursorInContent = content->mapFrom(viewport(), zoomAnchorViewport); + zoomAnchorNormX = std::clamp(double(cursorInContent.x()) / content->width(), 0.0, 1.0); + zoomAnchorNormY = std::clamp(double(cursorInContent.y()) / content->height(), 0.0, 1.0); + } else { + zoomAnchorNormX = 0.5; + zoomAnchorNormY = 0.5; + } +} + +bool Viewer::applyZoomAtAnchor(int newZoom) +{ + newZoom = std::clamp(newZoom, 30, 500); + if (newZoom == zoom) { + return false; + } + + if (continuousScroll) { + updateZoomRatio(newZoom); + } else { + const int previousZoom = zoom; + zoom = newZoom; + + if (!zoomPreviewActive) { + // Reuse the current high-quality pixmap while the label follows the requested + // geometry. The normal renderer replaces it after the interaction pauses. + scaledContentsBeforeZoomPreview = content->hasScaledContents(); + zoomPreviewBaseSize = content->size(); + zoomPreviewBaseZoom = previousZoom; + content->setScaledContents(true); + zoomPreviewActive = true; + } + + const double scale = static_cast(newZoom) / zoomPreviewBaseZoom; + content->resize(std::max(1, qRound(zoomPreviewBaseSize.width() * scale)), + std::max(1, qRound(zoomPreviewBaseSize.height() * scale))); + restoreZoomAnchor(); + zoomPreviewFinishTimer->start(); + } + + zoomHud->setText(QStringLiteral("%1%").arg(zoom)); + positionZoomHud(); + zoomHud->show(); + + emit zoomUpdated(zoom); + return true; +} + +void Viewer::restoreZoomAnchor() +{ + const int alignX = std::max(0, (viewport()->width() - content->width()) / 2); + const int alignY = std::max(0, (viewport()->height() - content->height()) / 2); + const int targetH = std::lround(zoomAnchorNormX * content->width()) + alignX - zoomAnchorViewport.x(); + const int targetV = std::lround(zoomAnchorNormY * content->height()) + alignY - zoomAnchorViewport.y(); + horizontalScrollBar()->setValue(targetH); + verticalScrollBar()->setValue(targetV); +} + +void Viewer::cancelZoomPreview() +{ + if (!zoomPreviewActive) { + return; + } + + zoomPreviewFinishTimer->stop(); + content->setScaledContents(scaledContentsBeforeZoomPreview); + zoomPreviewActive = false; +} + +void Viewer::renderFinalZoomImage() +{ + if (!zoomPreviewActive) { + return; + } + + cancelZoomPreview(); + updateContentSize(); + restoreZoomAnchor(); +} + +void Viewer::positionZoomHud() +{ + const int margin = 16; + zoomHud->move(width() - zoomHud->width() - margin, + height() - zoomHud->height() - margin); + zoomHud->raise(); +} + +bool Viewer::gestureEvent(QGestureEvent *event) +{ + if (QGesture *g = event->gesture(Qt::PinchGesture)) { + auto *pinch = static_cast(g); + if (!render->hasLoadedComic()) { + event->accept(pinch); + return true; + } + if (pinch->state() == Qt::GestureStarted) { + zoomHudHideTimer->stop(); + pinchStartZoom = zoom; + captureZoomAnchor(); + } + int newZoom = std::clamp(std::lround(pinchStartZoom * pinch->totalScaleFactor()), 30, 500); + applyZoomAtAnchor(newZoom); + if (pinch->state() == Qt::GestureFinished || pinch->state() == Qt::GestureCanceled) { + renderFinalZoomImage(); + zoomHud->hide(); + } + event->accept(pinch); + return true; + } + return QScrollArea::event(event); +} + void Viewer::setActiveWidget(QWidget *w) { if (widget() == w) { return; } + cancelZoomPreview(); verticalScrollBar()->blockSignals(true); takeWidget(); const bool isContinuous = (w == continuousWidget); @@ -1697,6 +2050,10 @@ void Viewer::updateConfig(QSettings *settings) { goToFlow->updateConfig(settings); + mglass->setCircular(Configuration::getConfiguration().getMagnifyingGlassCircular()); + mglass->setRing(Configuration::getConfiguration().getMagnifyingGlassRing()); + magnifierEdgeEase = Configuration::getConfiguration().getMagnifyingGlassEdgeEase(); + QPalette palette; palette.setColor(backgroundRole(), Configuration::getConfiguration().getBackgroundColor(theme.viewer.defaultBackgroundColor)); setPalette(palette); diff --git a/YACReader/viewer.h b/YACReader/viewer.h index 2393ff338..0e2441800 100644 --- a/YACReader/viewer.h +++ b/YACReader/viewer.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -18,6 +20,7 @@ #include #include #include +#include #include #include @@ -85,6 +88,8 @@ public slots: void rotateLeft(); void rotateRight(); bool magnifyingGlassIsVisible() const { return magnifyingGlassShown; } + bool translatorIsVisible() const; + bool goToFlowIsVisible() const; void setBookmark(bool); void save(); void doublePageSwitch(); @@ -176,13 +181,43 @@ public slots: bool magnifyingGlassShown; bool restoreMagnifyingGlass; void setMagnifyingGlassShown(bool shown); + //! When true, the loupe's sampled-region center is pushed non-linearly toward the + //! viewport edges so edge content is reachable with less cursor travel. + //! The push is applied per axis until the page is letterboxed by more than a fraction of + //! its own size on that axis (a thin margin, an exact fit, or an overflowing page still + //! ease); past that threshold the background beside the page is wide enough that easing + //! would mostly reveal it, so the axis is left at 1:1. + bool magnifierEdgeEase; //! Event handlers: void resizeEvent(QResizeEvent *event) override; void wheelEvent(QWheelEvent *event) override; void wheelEventMouse(QWheelEvent *event); void wheelEventTrackpad(QWheelEvent *event); + void wheelEventZoom(QWheelEvent *event); void mouseMoveEvent(QMouseEvent *event) override; + bool event(QEvent *event) override; + bool gestureEvent(QGestureEvent *event); + void captureZoomAnchor(); + bool applyZoomAtAnchor(int newZoom); + void restoreZoomAnchor(); + void cancelZoomPreview(); + void renderFinalZoomImage(); + void positionZoomHud(); + + int pinchStartZoom; + QPoint zoomAnchorViewport; + double zoomAnchorNormX; + double zoomAnchorNormY; + QLabel *zoomHud; + QTimer *zoomHudHideTimer; + QTimer *zoomPreviewFinishTimer; + bool zoomPreviewActive = false; + bool scaledContentsBeforeZoomPreview = false; + QSize zoomPreviewBaseSize; + int zoomPreviewBaseZoom = 100; + int wheelZoomAccumulator = 0; + QElapsedTimer wheelZoomTimer; int verticalScrollStep() const; int horizontalScrollStep() const; @@ -231,6 +266,13 @@ public slots: QByteArray rawPage(int page) const; QList currentVisiblePages(); QImage grabMagnifiedRegion(const QPoint &viewerPos, const QSize &glassSize, float zoomLevel) const; + //! Eases a cursor position (viewport coords) toward the edges to give the loupe's + //! *content* its sampled-region center, normalized against the viewport. The outward push + //! is bounded by the loupe's own half-size (per-axis for a rect, radially for a circle), + //! so the cursor's point stays inside the loupe view and the strength scales with loupe + //! size; it is skipped on an axis only where the page is letterboxed beyond a fraction of + //! its own size. + QPoint easeViewerPos(const QPoint &viewerPos, const QSize &glassSize, bool circular) const; // Comic * getComic(){return comic;} const BookmarksDialog *getBookmarksDialog() { return bd; } // returns the current index starting in 1 [1,nPages] diff --git a/YACReader/yacreader_de.ts b/YACReader/yacreader_de.ts index c99d24f20..70c25659e 100644 --- a/YACReader/yacreader_de.ts +++ b/YACReader/yacreader_de.ts @@ -92,24 +92,24 @@ Der aktuelle Theme-JSON konnte nicht geladen werden. - + Import theme Thema importieren - + JSON files (*.json);;All files (*) JSON-Dateien (*.json);;Alle Dateien (*) - + Could not import theme from: %1 Theme konnte nicht importiert werden von: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Der Import ist fehlgeschlagen @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Seite wird geladen %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported Format wird nicht unterstützt - + 7z not found 7z nicht gefunden - + Unknown error opening the file Unbekannter Fehler beim Öffnen des Files - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly CRC Error auf Seite (%1): Einige Seiten werden nicht korrekt dargestellt @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Hilfe - + System info Systeminformationen - + + Changelog + + + + About Über @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Gammawert - + Reset Zurücksetzen @@ -281,62 +291,62 @@ Meine Comics-Pfad - + Scaling Skalierung - + Scaling method Skalierungsmethode - + Nearest (fast, low quality) Am nächsten (schnell, niedrige Qualität) - + Bilinear Bilinear-Filter - + Lanczos (better quality) Lanczos (bessere Qualität) - + Image adjustment Bildanpassung - + "Go to flow" size Größe von "Gehe zu Comic Flow" - + Choose Auswählen - + Image options Bilderoptionen - + Contrast Kontrast - + Appearance Aussehen - + Options Optionen @@ -356,42 +366,42 @@ Systemstandard - + Clear Löschen - + Comics directory Comics-Verzeichnis - + Background color Hintergrundfarbe - + Page Flow Seitenfluss - + General Allgemein - + Brightness Helligkeit - + Restart is needed Neustart erforderlich - + Quick Navigation Mode Schnellnavigations-Modus @@ -406,67 +416,127 @@ Zeit im Informationsetikett der aktuellen Seite anzeigen - + + Magnifying glass + Vergrößerungsglas + + + + Circular magnifying glass + Kreisförmiges Vergrößerungsglas + + + + Draw a ring around the circular magnifying glass + Einen Ring um das kreisförmige Vergrößerungsglas zeichnen + + + + Ease cursor movement toward the edges + Cursorbewegung zu den Rändern hin abfedern + + + Scroll behaviour Scrollverhalten - + Disable scroll animations and smooth scrolling Scroll-Animationen und sanftes Scrollen deaktivieren - + Do not turn page using scroll Blättern Sie nicht mit dem Scrollen um - + Use single scroll step to turn page Verwenden Sie einen einzelnen Bildlaufschritt, um die Seite umzublättern - + Mouse mode Mausmodus - + Only Back/Forward buttons can turn pages Nur mit den Zurück-/Vorwärts-Tasten können Seiten umgeblättert werden - + Use the Left/Right buttons to turn pages. Verwenden Sie die Links-/Rechts-Tasten, um Seiten umzublättern. - + Click left or right half of the screen to turn pages. Klicken Sie auf die linke oder rechte Hälfte des Bildschirms, um die Seiten umzublättern. - + + Escape key + Escape-Taste + + + + Quit the reader + Reader beenden + + + + Cancel the active mode + Aktiven Modus abbrechen + + + + Escape closes the reader, even while a mode is active. + Die Escape-Taste schließt den Reader, auch wenn ein Modus aktiv ist. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Die Escape-Taste beendet den ersten aktiven Modus in dieser Reihenfolge: + +1. Lupe +2. Wörterbuch +3. „Gehe zu Seite“-Leiste +4. Vollbildmodus + +Wenn keiner aktiv ist, geschieht beim Drücken der Escape-Taste nichts. + + + Disable mouse over activation Aktivierung durch Maus deaktivieren - + Fit options Anpassungsoptionen - + Enlarge images to fit width/height Bilder vergrößern, um sie Breite/Höhe anzupassen - + Double Page options Doppelseiten-Einstellungen - + Show covers as single page Cover als eine Seite darstellen @@ -704,48 +774,48 @@ Viewer - + Page not available! Seite nicht verfügbar! - - + + Press 'O' to open comic. 'O' drücken, um Comic zu öffnen. - + Error opening comic Fehler beim Öffnen des Comics - + Cover! Titelseite! - + CRC Error CRC Fehler - + Comic not found Comic nicht gefunden - + Not found Nicht gefunden - + Last page! Letzte Seite! - + Loading...please wait! Ladevorgang... Bitte warten! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Öffnen - + Open a comic Comic öffnen - + New instance Neuer Fall - + Open Folder Ordner öffnen - + Open image folder Bilder-Ordner öffnen - + Open latest comic Neuesten Comic öffnen - + Open the latest comic opened in the previous reading session Öffne den neuesten Comic deiner letzten Sitzung - + Clear Löschen - + Clear open recent list Lösche Liste zuletzt geöffneter Elemente - + Save Speichern - - + + Save current page Aktuelle Seite speichern - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Voheriger Comic - - - + + + Open previous comic Vorherigen Comic öffnen - + Next Comic Nächster Comic - - - + + + Open next comic Nächsten Comic öffnen - + &Previous &Vorherige - - - + + + Go to previous page Zur vorherigen Seite gehen - + &Next &Nächstes - - - + + + Go to next page Zur nächsten Seite gehen - + Fit Height Höhe anpassen - + Fit image to height Bild an Höhe anpassen - + Fit Width Breite anpassen - + Fit image to width Bildbreite anpassen - + Show full size Vollansicht anzeigen - + Fit to page An Seite anpassen - + Continuous scroll Kontinuierliches Scrollen - + Switch to continuous scroll mode Wechseln Sie in den kontinuierlichen Bildlaufmodus - + Reset zoom Zoom zurücksetzen - + Show zoom slider Zoomleiste anzeigen - + Zoom+ Vergr??ern+ - + Zoom- Verkleinern- - + Rotate image to the left Bild nach links drehen - + Rotate image to the right Bild nach rechts drehen - + Double page mode Doppelseiten-Modus - + Switch to double page mode Zum Doppelseiten-Modus wechseln - + Double page manga mode Doppelseiten-Manga-Modus - + Reverse reading order in double page mode Umgekehrte Lesereihenfolge im Doppelseiten-Modus - + Go To Gehe zu - + Go to page ... Gehe zu Seite ... - + Options Optionen - + YACReader options YACReader Optionen - - + + Help Hilfe - + Help, About YACReader Hilfe, über YACReader - + Magnifying glass Vergößerungsglas - + Switch Magnifying glass Vergrößerungsglas wechseln - + Set bookmark Lesezeichen setzen - + Set a bookmark on the current page Lesezeichen auf dieser Seite setzen - + Show bookmarks Lesezeichen anzeigen - + Show the bookmarks of the current comic Lesezeichen für diesen Comic anzeigen - + Show keyboard shortcuts Tastenkürzel anzeigen - + Show Info Info anzeigen - + + Escape + Escape + + + + Escape key: quit, or cancel the active mode + Escape-Taste: Reader beenden oder aktiven Modus abbrechen + + + Close Schliessen - + Show Dictionary Wörterbuch anzeigen - + Show go to flow "Gehe zu Comic Flow" anzeigen - + Edit shortcuts Kürzel ändern - + &File &Datei - - + + Open recent Kürzlich geöffnet - + File Datei - + Edit Ändern - + View Anzeigen - + Go Los - + Window Fenster - - - + Open Comic Comic öffnen - - - + Comic files Comic-Dateien - + Open folder Ordner öffnen - - + + Comics Comichefte - + Toggle fullscreen mode Vollbild-Modus umschalten - + Hide/show toolbar Symbolleiste anzeigen/verstecken - - + + General Allgemein - + Size up magnifying glass Vergrößerungsglas vergrößern - + Size down magnifying glass Vergrößerungsglas verkleinern - + Zoom in magnifying glass Vergrößerungsglas reinzoomen - + Zoom out magnifying glass Vergrößerungsglas rauszoomen - + Reset magnifying glass Lupe zurücksetzen - - + + Magnifiying glass Vergrößerungsglas - + Toggle between fit to width and fit to height Zwischen Anpassung an Seite und Höhe wechseln - - + + Page adjustement Seitenanpassung - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Automatisches Runterscrollen - + Autoscroll up Automatisches Raufscrollen - + Autoscroll forward, horizontal first Automatisches Vorwärtsscrollen, horizontal zuerst - + Autoscroll backward, horizontal first Automatisches Zurückscrollen, horizontal zuerst - + Autoscroll forward, vertical first Automatisches Vorwärtsscrollen, vertikal zuerst - + Autoscroll backward, vertical first Automatisches Zurückscrollen, vertikal zuerst - + Move down Nach unten - + Move up Nach oben - + Move left Nach links - + Move right Nach rechts - + Go to the first page Zur ersten Seite gehen - + Go to the last page Zur letzten Seite gehen - + Offset double page to the left Doppelseite nach links versetzt - + Offset double page to the right Doppelseite nach rechts versetzt - - + + Reading Lesend - + There is a new version available Neue Version verfügbar - + Do you want to download the new version? Möchten Sie die neue Version herunterladen? - + Remind me in 14 days In 14 Tagen erneut erinnern - + Not now Nicht jetzt @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Speichern - + Cancel Abbrechen - + + Keyboard shortcuts + Tastenkürzel + + + + Customize the keyboard shortcuts used by the application. + Passen Sie die von der Anwendung verwendeten Tastenkürzel an. + + + Edit shortcuts Kürzel bearbeiten - + Shortcuts Kürzel diff --git a/YACReader/yacreader_en.ts b/YACReader/yacreader_en.ts index 805266d02..71ff4c863 100644 --- a/YACReader/yacreader_en.ts +++ b/YACReader/yacreader_en.ts @@ -92,24 +92,24 @@ The current theme JSON could not be loaded. - + Import theme Import theme - + JSON files (*.json);;All files (*) JSON files (*.json);;All files (*) - + Could not import theme from: %1 Could not import theme from: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Import failed @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Loading page %1 @@ -188,25 +188,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly CRC error on page (%1): some of the pages will not be displayed correctly - + Unknown error opening the file Unknown error opening the file - + 7z not found 7z not found - + Format not supported Format not supported + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,25 +253,30 @@ HelpAboutDialog - + About About - + Help Help - + System info System info + + + Changelog + + OptionsDialog - + "Go to flow" size "Go to flow" size @@ -276,57 +286,57 @@ My comics path - + Background color Background color - + Choose Choose - + Quick Navigation Mode Quick Navigation Mode - + Disable mouse over activation Disable mouse over activation - + Scaling Scaling - + Scaling method Scaling method - + Nearest (fast, low quality) Nearest (fast, low quality) - + Bilinear Bilinear - + Lanczos (better quality) Lanczos (better quality) - + Restart is needed Restart is needed - + Brightness Brightness @@ -356,117 +366,177 @@ Show time in current page information label - + + Magnifying glass + Magnifying glass + + + + Circular magnifying glass + Circular magnifying glass + + + + Draw a ring around the circular magnifying glass + Draw a ring around the circular magnifying glass + + + + Ease cursor movement toward the edges + Ease cursor movement toward the edges + + + Scroll behaviour Scroll behaviour - + Disable scroll animations and smooth scrolling Disable scroll animations and smooth scrolling - + Do not turn page using scroll Do not turn page using scroll - + Use single scroll step to turn page Use single scroll step to turn page - + Mouse mode Mouse mode - + Only Back/Forward buttons can turn pages Only Back/Forward buttons can turn pages - + Use the Left/Right buttons to turn pages. Use the Left/Right buttons to turn pages. - + Click left or right half of the screen to turn pages. Click left or right half of the screen to turn pages. - + + Escape key + Escape key + + + + Quit the reader + Quit the reader + + + + Cancel the active mode + Cancel the active mode + + + + Escape closes the reader, even while a mode is active. + Escape closes the reader, even while a mode is active. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + + + Contrast Contrast - + Gamma Gamma - + Reset Reset - + Image options Image options - + Fit options Fit options - + Enlarge images to fit width/height Enlarge images to fit width/height - + Double Page options Double Page options - + Show covers as single page Show covers as single page - + General General - + Appearance Appearance - + Clear Clear - + Page Flow Page Flow - + Image adjustment Image adjustment - + Options Options - + Comics directory Comics directory @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. Press 'O' to open comic. - + Not found Not found - + Comic not found Comic not found - + Error opening comic Error opening comic - + CRC Error CRC Error - + Loading...please wait! Loading...please wait! - + Page not available! Page not available! - + Cover! Cover! - + Last page! Last page! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Open - + Open a comic Open a comic - + New instance New instance - + Open Folder Open Folder - + Open image folder Open image folder - + Open latest comic Open latest comic - + Open the latest comic opened in the previous reading session Open the latest comic opened in the previous reading session - + Clear Clear - + Clear open recent list Clear open recent list - + Save Save - - + + Save current page Save current page - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Previous Comic - - - + + + Open previous comic Open previous comic - + Next Comic Next Comic - - - + + + Open next comic Open next comic - + &Previous &Previous - - - + + + Go to previous page Go to previous page - + &Next &Next - - - + + + Go to next page Go to next page - + Fit Height Fit Height - + Fit image to height Fit image to height - + Fit Width Fit Width - + Fit image to width Fit image to width - + Show full size Show full size - + Fit to page Fit to page - + Continuous scroll Continuous scroll - + Switch to continuous scroll mode Switch to continuous scroll mode - + Reset zoom Reset zoom - + Show zoom slider Show zoom slider - + Zoom+ Zoom+ - + Zoom- Zoom- - + Rotate image to the left Rotate image to the left - + Rotate image to the right Rotate image to the right - + Double page mode Double page mode - + Switch to double page mode Switch to double page mode - + Double page manga mode Double page manga mode - + Reverse reading order in double page mode Reverse reading order in double page mode - + Go To Go To - + Go to page ... Go to page ... - + Options Options - + YACReader options YACReader options - - + + Help Help - + Help, About YACReader Help, About YACReader - + Magnifying glass Magnifying glass - + Switch Magnifying glass Switch Magnifying glass - + Set bookmark Set bookmark - + Set a bookmark on the current page Set a bookmark on the current page - + Show bookmarks Show bookmarks - + Show the bookmarks of the current comic Show the bookmarks of the current comic - + Show keyboard shortcuts Show keyboard shortcuts - + Show Info Show Info - + + Escape + Escape + + + + Escape key: quit, or cancel the active mode + Escape key: quit, or cancel the active mode + + + Close Close - + Show Dictionary Show Dictionary - + Show go to flow Show go to flow - + Edit shortcuts Edit shortcuts - + &File &File - - + + Open recent Open recent - + File File - + Edit Edit - + View View - + Go Go - + Window Window - - - + Open Comic Open Comic - - - + Comic files Comic files - + Open folder Open folder - - + + Comics Comics - + Toggle fullscreen mode Toggle fullscreen mode - + Hide/show toolbar Hide/show toolbar - - + + General General - + Size up magnifying glass Size up magnifying glass - + Size down magnifying glass Size down magnifying glass - + Zoom in magnifying glass Zoom in magnifying glass - + Zoom out magnifying glass Zoom out magnifying glass - + Reset magnifying glass Reset magnifying glass - - + + Magnifiying glass Magnifiying glass - + Toggle between fit to width and fit to height Toggle between fit to width and fit to height - - + + Page adjustement Page adjustement - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Autoscroll down - + Autoscroll up Autoscroll up - + Autoscroll forward, horizontal first Autoscroll forward, horizontal first - + Autoscroll backward, horizontal first Autoscroll backward, horizontal first - + Autoscroll forward, vertical first Autoscroll forward, vertical first - + Autoscroll backward, vertical first Autoscroll backward, vertical first - + Move down Move down - + Move up Move up - + Move left Move left - + Move right Move right - + Go to the first page Go to the first page - + Go to the last page Go to the last page - + Offset double page to the left Offset double page to the left - + Offset double page to the right Offset double page to the right - - + + Reading Reading - + There is a new version available There is a new version available - + Do you want to download the new version? Do you want to download the new version? - + Remind me in 14 days Remind me in 14 days - + Not now Not now @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Save - + Cancel Cancel - + + Keyboard shortcuts + Keyboard shortcuts + + + + Customize the keyboard shortcuts used by the application. + Customize the keyboard shortcuts used by the application. + + + Edit shortcuts Edit shortcuts - + Shortcuts Shortcuts diff --git a/YACReader/yacreader_es.ts b/YACReader/yacreader_es.ts index 5f79c4d6b..19c9df467 100644 --- a/YACReader/yacreader_es.ts +++ b/YACReader/yacreader_es.ts @@ -92,24 +92,24 @@ No se ha podido cargar el JSON del tema actual. - + Import theme Importar tema - + JSON files (*.json);;All files (*) Archivos JSON (*.json);;Todos los archivos (*) - + Could not import theme from: %1 No se pudo importar el tema desde: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Error al importar @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Cargando página %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported Formato no soportado - + 7z not found 7z no encontrado - + Unknown error opening the file Error desconocido abriendo el archivo - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Error CRC en la página (%1): algunas de las páginas no se mostrarán correctamente @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Ayuda - + System info Información de sistema - + + Changelog + + + + About Acerca de @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Gama - + Reset Restablecer @@ -281,62 +291,62 @@ Ruta a mis cómics - + Scaling Escalado - + Scaling method Método de escalado - + Nearest (fast, low quality) Vecino más cercano (rápido, baja calidad) - + Bilinear Bilineal - + Lanczos (better quality) Lanczos (mejor calidad) - + Image adjustment Ajustes de imagen - + "Go to flow" size Tamaño de "Ir a Comic Flow" - + Choose Elegir - + Image options Opciones de imagen - + Contrast Contraste - + Appearance Apariencia - + Options Opciones @@ -356,42 +366,42 @@ Predeterminado del sistema - + Clear Limpiar - + Comics directory Directorio de cómics - + Background color Color de fondo - + Page Flow Flujo de página - + General Opciones generales - + Brightness Brillo - + Restart is needed Es necesario reiniciar - + Quick Navigation Mode Modo de navegación rápida @@ -406,67 +416,127 @@ Mostrar la hora en la etiqueta de información de la página actual - + + Magnifying glass + Lupa + + + + Circular magnifying glass + Lupa circular + + + + Draw a ring around the circular magnifying glass + Dibujar un anillo alrededor de la lupa circular + + + + Ease cursor movement toward the edges + Suavizar el movimiento del cursor hacia los bordes + + + Scroll behaviour Comportamiento del scroll - + Disable scroll animations and smooth scrolling Desactivar animaciones de desplazamiento y desplazamiento suave - + Do not turn page using scroll No cambiar de página usando el scroll - + Use single scroll step to turn page Usar un solo paso de desplazamiento para cambiar de página - + Mouse mode Modo del ratón - + Only Back/Forward buttons can turn pages Solo los botones Atrás/Adelante pueden cambiar de página - + Use the Left/Right buttons to turn pages. Usar los botones Izquierda/Derecha para cambiar de página. - + Click left or right half of the screen to turn pages. Hacer clic en la mitad izquierda o derecha de la pantalla para cambiar de página. - + + Escape key + Tecla Esc + + + + Quit the reader + Salir del lector + + + + Cancel the active mode + Cancelar el modo activo + + + + Escape closes the reader, even while a mode is active. + La tecla Esc cierra el lector, incluso cuando hay un modo activo. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + La tecla Esc cancela el primero de estos modos que esté activo: + +1. Lupa +2. Diccionario +3. Barra Ir a página +4. Pantalla completa + +Si ninguno está activo, la tecla Esc no hace nada. + + + Disable mouse over activation Desactivar activación al pasar el ratón - + Fit options Opciones de ajuste - + Enlarge images to fit width/height Ampliar imágenes para ajustarse al ancho/alto - + Double Page options Opciones de doble página - + Show covers as single page Mostrar portadas como página única @@ -704,48 +774,48 @@ Viewer - + Page not available! ¡Página no disponible! - - + + Press 'O' to open comic. Pulsa 'O' para abrir un fichero. - + Error opening comic Error abriendo cómic - + Cover! ¡Portada! - + CRC Error Error CRC - + Comic not found Cómic no encontrado - + Not found No encontrado - + Last page! ¡Última página! - + Loading...please wait! Cargando...espere, por favor! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Abrir - + Open a comic Abrir cómic - + New instance Nueva instancia - + Open Folder Abrir carpeta - + Open image folder Abrir carpeta de imágenes - + Open latest comic Abrir el cómic más reciente - + Open the latest comic opened in the previous reading session Abrir el cómic más reciente abierto en la sesión de lectura anterior - + Clear Limpiar - + Clear open recent list Limpiar lista de abiertos recientemente - + Save Guardar - - + + Save current page Guardar la página actual - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Cómic anterior - - - + + + Open previous comic Abrir cómic anterior - + Next Comic Siguiente Cómic - - - + + + Open next comic Abrir siguiente cómic - + &Previous A&nterior - - - + + + Go to previous page Ir a la página anterior - + &Next Siguie&nte - - - + + + Go to next page Ir a la página siguiente - + Fit Height Ajustar altura - + Fit image to height Ajustar página a lo alto - + Fit Width Ajustar anchura - + Fit image to width Ajustar página a lo ancho - + Show full size Mostrar a tamaño original - + Fit to page Ajustar a página - + Continuous scroll Desplazamiento continuo - + Switch to continuous scroll mode Cambiar al modo de desplazamiento continuo - + Reset zoom Restablecer zoom - + Show zoom slider Mostrar control deslizante de zoom - + Zoom+ Ampliar+ - + Zoom- Reducir - + Rotate image to the left Rotar imagen a la izquierda - + Rotate image to the right Rotar imagen a la derecha - + Double page mode Modo a doble página - + Switch to double page mode Cambiar a modo de doble página - + Double page manga mode Modo de manga de página doble - + Reverse reading order in double page mode Invertir el orden de lectura en modo de página doble - + Go To Ir a - + Go to page ... Ir a página... - + Options Opciones - + YACReader options Opciones de YACReader - - + + Help Ayuda - + Help, About YACReader Ayuda, Sobre YACReader - + Magnifying glass Lupa - + Switch Magnifying glass Lupa On/Off - + Set bookmark Añadir marcador - + Set a bookmark on the current page Añadir un marcador en la página actual - + Show bookmarks Mostrar marcadores - + Show the bookmarks of the current comic Mostrar los marcadores del cómic actual - + Show keyboard shortcuts Mostrar atajos de teclado - + Show Info Mostrar información - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Tecla Esc: salir o cancelar el modo activo + + + Close Cerrar - + Show Dictionary Mostrar diccionario - + Show go to flow Mostrar "Ir a Comic Flow" - + Edit shortcuts Editar accesos directos - + &File &Archivo - - + + Open recent Abrir reciente - + File Archivo - + Edit Editar - + View Ver - + Go Ir - + Window Ventana - - - + Open Comic Abrir cómic - - - + Comic files Archivos de cómic - + Open folder Abrir carpeta - - + + Comics Cómics - + Toggle fullscreen mode Alternar modo de pantalla completa - + Hide/show toolbar Ocultar/mostrar barra de herramientas - - + + General Opciones generales - + Size up magnifying glass Aumentar tamaño de la lupa - + Size down magnifying glass Disminuir tamaño de lupa - + Zoom in magnifying glass Incrementar el aumento de la lupa - + Zoom out magnifying glass Reducir el aumento de la lupa - + Reset magnifying glass Resetear lupa - - + + Magnifiying glass Lupa - + Toggle between fit to width and fit to height Alternar entre ajuste al ancho y ajuste al alto - - + + Page adjustement Ajuste de página - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Desplazamiento automático hacia abajo - + Autoscroll up Desplazamiento automático hacia arriba - + Autoscroll forward, horizontal first Desplazamiento automático hacia adelante, primero horizontal - + Autoscroll backward, horizontal first Desplazamiento automático hacia atrás, primero horizontal - + Autoscroll forward, vertical first Desplazamiento automático hacia adelante, primero vertical - + Autoscroll backward, vertical first Desplazamiento automático hacia atrás, primero vertical - + Move down Mover abajo - + Move up Mover arriba - + Move left Mover a la izquierda - + Move right Mover a la derecha - + Go to the first page Ir a la primera página - + Go to the last page Ir a la última página - + Offset double page to the left Mover una página a la izquierda - + Offset double page to the right Mover una página a la derecha - - + + Reading Leyendo - + There is a new version available Hay una nueva versión disponible - + Do you want to download the new version? ¿Desea descargar la nueva versión? - + Remind me in 14 days Recordar en 14 días - + Not now Ahora no @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Guardar - + Cancel Cancelar - + + Keyboard shortcuts + Atajos de teclado + + + + Customize the keyboard shortcuts used by the application. + Personaliza los atajos de teclado utilizados por la aplicación. + + + Edit shortcuts Editar accesos directos - + Shortcuts Accesos directos diff --git a/YACReader/yacreader_fr.ts b/YACReader/yacreader_fr.ts index 96e15a969..f86d19b94 100644 --- a/YACReader/yacreader_fr.ts +++ b/YACReader/yacreader_fr.ts @@ -92,24 +92,24 @@ Le thème actuel JSON n'a pas pu être chargé. - + Import theme Importer un thème - + JSON files (*.json);;All files (*) Fichiers JSON (*.json);;Tous les fichiers (*) - + Could not import theme from: %1 Impossible d'importer le thème depuis : %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Échec de l'importation @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Chargement de la page %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported Format non supporté - + 7z not found 7z introuvable - + Unknown error opening the file Erreur inconnue lors de l'ouverture du fichier - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Erreur CRC sur la page (%1): certaines pages ne s'afficheront pas correctement @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Aide - + System info Informations système - + + Changelog + + + + About A propos @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Valeur gamma - + Reset Remise à zéro @@ -281,37 +291,37 @@ Chemin de mes bandes dessinées - + Image adjustment Ajustement de l'image - + "Go to flow" size Taille de "Aller à Comic Flow" - + Choose Choisir - + Image options Option de l'image - + Contrast Contraste - + Appearance Apparence - + Options Possibilités @@ -331,17 +341,17 @@ Par défaut du système - + Clear Clair - + Comics directory Répertoire des bandes dessinées - + Quick Navigation Mode Mode navigation rapide @@ -356,117 +366,177 @@ Afficher l'heure dans l'étiquette d'information de la page actuelle - + + Magnifying glass + Loupe + + + + Circular magnifying glass + Loupe circulaire + + + + Draw a ring around the circular magnifying glass + Tracer un anneau autour de la loupe circulaire + + + + Ease cursor movement toward the edges + Adoucir le déplacement du curseur vers les bords + + + Background color Couleur d'arrière plan - + Scroll behaviour Comportement de défilement - + Disable scroll animations and smooth scrolling Désactiver les animations de défilement et le défilement fluide - + Do not turn page using scroll Ne tournez pas la page en utilisant le défilement - + Use single scroll step to turn page Utilisez une seule étape de défilement pour tourner la page - + Mouse mode Mode souris - + Only Back/Forward buttons can turn pages Seuls les boutons Précédent/Avant peuvent tourner les pages - + Use the Left/Right buttons to turn pages. Utilisez les boutons Gauche/Droite pour tourner les pages. - + Click left or right half of the screen to turn pages. Cliquez sur la moitié gauche ou droite de l'écran pour tourner les pages. - + + Escape key + Touche Échap + + + + Quit the reader + Quitter le lecteur + + + + Cancel the active mode + Annuler le mode actif + + + + Escape closes the reader, even while a mode is active. + La touche Échap ferme le lecteur, même lorsqu’un mode est actif. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + La touche Échap annule le premier des modes actifs suivants : + +1. Loupe +2. Dictionnaire +3. Barre Aller à la page +4. Plein écran + +Si aucun n’est actif, la touche Échap ne fait rien. + + + Disable mouse over activation Désactiver la souris sur l'activation - + Scaling Mise à l'échelle - + Scaling method Méthode de mise à l'échelle - + Nearest (fast, low quality) Le plus proche (rapide, mauvaise qualité) - + Bilinear Bilinéaire - + Lanczos (better quality) Lanczos (meilleure qualité) - + Page Flow Flux des pages - + General Général - + Brightness Luminosité - + Restart is needed Redémarrage nécessaire - + Fit options Options d'ajustement - + Enlarge images to fit width/height Agrandir les images pour les adapter à la largeur/hauteur - + Double Page options Options de double page - + Show covers as single page Afficher les couvertures sur une seule page @@ -704,48 +774,48 @@ Viewer - + Page not available! Page non disponible ! - - + + Press 'O' to open comic. Appuyez sur "O" pour ouvrir une bande dessinée. - + Error opening comic Erreur d'ouverture de la bande dessinée - + Cover! Couverture! - + CRC Error Erreur CRC - + Comic not found Bande dessinée introuvable - + Not found Introuvable - + Last page! Dernière page! - + Loading...please wait! Chargement... Patientez @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Ouvrir - + Open a comic Ouvrir une bande dessinée - + New instance Nouvelle instance - + Open Folder Ouvrir un dossier - + Open image folder Ouvrir un dossier d'images - + Open latest comic Ouvrir la dernière bande dessinée - + Open the latest comic opened in the previous reading session Ouvrir la dernière bande dessinée ouverte lors de la session de lecture précédente - + Clear Clair - + Clear open recent list Vider la liste d'ouverture récente - + Save Sauvegarder - - + + Save current page Sauvegarder la page actuelle - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Bande dessinée précédente - - - + + + Open previous comic Ouvrir la bande dessiné précédente - + Next Comic Bande dessinée suivante - - - + + + Open next comic Ouvrir la bande dessinée suivante - + &Previous &Précédent - - - + + + Go to previous page Aller à la page précédente - + &Next &Suivant - - - + + + Go to next page Aller à la page suivante - + Fit Height Ajuster la hauteur - + Fit image to height Ajuster l'image à la hauteur - + Fit Width Ajuster la largeur - + Fit image to width Ajuster l'image à la largeur - + Show full size Plein écran - + Fit to page Ajuster à la page - + Continuous scroll Défilement continu - + Switch to continuous scroll mode Passer en mode défilement continu - + Reset zoom Réinitialiser le zoom - + Show zoom slider Afficher le curseur de zoom - + Zoom+ Agrandir - + Zoom- R?duire - + Rotate image to the left Rotation à gauche - + Rotate image to the right Rotation à droite - + Double page mode Mode double page - + Switch to double page mode Passer en mode double page - + Double page manga mode Mode manga en double page - + Reverse reading order in double page mode Ordre de lecture inversée en mode double page - + Go To Aller à - + Go to page ... Aller à la page ... - + Options Possibilités - + YACReader options Options de YACReader - - + + Help Aide - + Help, About YACReader Aide, à propos de YACReader - + Magnifying glass Loupe - + Switch Magnifying glass Utiliser la loupe - + Set bookmark Placer un marque-page - + Set a bookmark on the current page Placer un marque-page sur la page actuelle - + Show bookmarks Voir les marque-pages - + Show the bookmarks of the current comic Voir les marque-pages de cette bande dessinée - + Show keyboard shortcuts Voir les raccourcis - + Show Info Voir les infos - + + Escape + Échap + + + + Escape key: quit, or cancel the active mode + Touche Échap : quitter ou annuler le mode actif + + + Close Fermer - + Show Dictionary Dictionnaire - + Show go to flow Afficher "Aller à Comic Flow" - + Edit shortcuts Modifier les raccourcis - + &File &Fichier - - + + Open recent Ouvrir récent - + File Fichier - + Edit Editer - + View Vue - + Go Aller - + Window Fenêtre - - - + Open Comic Ouvrir la bande dessinée - - - + Comic files Bande dessinée - + Open folder Ouvirir le dossier - - + + Comics Bandes dessinées - + Toggle fullscreen mode Basculer en mode plein écran - + Hide/show toolbar Masquer / afficher la barre d'outils - - + + General Général - + Size up magnifying glass Augmenter la taille de la loupe - + Size down magnifying glass Réduire la taille de la loupe - + Zoom in magnifying glass Zoomer - + Zoom out magnifying glass Dézoomer - + Reset magnifying glass Réinitialiser la loupe - - + + Magnifiying glass Loupe - + Toggle between fit to width and fit to height Basculer entre adapter à la largeur et adapter à la hauteur - - + + Page adjustement Ajustement de la page - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Défilement automatique vers le bas - + Autoscroll up Défilement automatique vers le haut - + Autoscroll forward, horizontal first Défilement automatique en avant, horizontal - + Autoscroll backward, horizontal first Défilement automatique en arrière horizontal - + Autoscroll forward, vertical first Défilement automatique en avant, vertical - + Autoscroll backward, vertical first Défilement automatique en arrière, verticak - + Move down Descendre - + Move up Monter - + Move left Déplacer à gauche - + Move right Déplacer à droite - + Go to the first page Aller à la première page - + Go to the last page Aller à la dernière page - + Offset double page to the left Double page décalée vers la gauche - + Offset double page to the right Double page décalée à droite - - + + Reading Lecture - + There is a new version available Une nouvelle version est disponible - + Do you want to download the new version? Voulez-vous télécharger la nouvelle version? - + Remind me in 14 days Rappelez-moi dans 14 jours - + Not now Pas maintenant @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Sauvegarder - + Cancel Annuler - + Shortcuts Raccourcis - + + Keyboard shortcuts + Raccourcis clavier + + + + Customize the keyboard shortcuts used by the application. + Personnalisez les raccourcis clavier utilisés par l’application. + + + Edit shortcuts Modifier les raccourcis diff --git a/YACReader/yacreader_it.ts b/YACReader/yacreader_it.ts index dc5db767a..f6271e7a2 100644 --- a/YACReader/yacreader_it.ts +++ b/YACReader/yacreader_it.ts @@ -92,24 +92,24 @@ Impossibile caricare il tema corrente JSON. - + Import theme Importa tema - + JSON files (*.json);;All files (*) File JSON (*.json);;Tutti i file (*) - + Could not import theme from: %1 Impossibile importare il tema da: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Importazione non riuscita @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Caricamento pagina %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported Formato non supportato - + 7z not found 7z non trovato - + Unknown error opening the file Errore sconosciuto aprendo il file - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Errore CRC alla pagina (%1): alcune pagine non saranno visualizzate correttamente @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Aiuto - + System info Informazioni di sistema - + + Changelog + + + + About Informazioni @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Valore gamma - + Reset Resetta @@ -281,37 +291,37 @@ Percorso dei miei fumetti - + Image adjustment Correzioni immagine - + "Go to flow" size Dimensione di "Vai a Comic Flow" - + Choose Scegli - + Image options Opzione immagine - + Contrast Contrasto - + Appearance Aspetto - + Options Opzioni @@ -331,17 +341,17 @@ Predefinita del sistema - + Clear Cancella - + Comics directory Cartella Fumetti - + Quick Navigation Mode Modo navigazione rapida @@ -356,117 +366,177 @@ Mostra l'ora nell'etichetta delle informazioni della pagina corrente - + + Magnifying glass + Lente d'ingrandimento + + + + Circular magnifying glass + Lente d'ingrandimento circolare + + + + Draw a ring around the circular magnifying glass + Disegna un anello intorno alla lente d'ingrandimento circolare + + + + Ease cursor movement toward the edges + Rendi più fluido il movimento del cursore verso i bordi + + + Background color Colore di sfondo - + Scroll behaviour Comportamento di scorrimento - + Disable scroll animations and smooth scrolling Disabilita le animazioni di scorrimento e lo scorrimento fluido - + Do not turn page using scroll Non voltare pagina utilizzando lo scorrimento - + Use single scroll step to turn page Utilizzare un singolo passaggio di scorrimento per voltare pagina - + Mouse mode Modalità mouse - + Only Back/Forward buttons can turn pages Solo i pulsanti Indietro/Avanti possono girare le pagine - + Use the Left/Right buttons to turn pages. Utilizzare i pulsanti Sinistra/Destra per girare le pagine. - + Click left or right half of the screen to turn pages. Fare clic sulla metà sinistra o destra dello schermo per girare le pagine. - + + Escape key + Tasto Esc + + + + Quit the reader + Esci dal lettore + + + + Cancel the active mode + Annulla la modalità attiva + + + + Escape closes the reader, even while a mode is active. + Il tasto Esc chiude il lettore, anche quando è attiva una modalità. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Il tasto Esc annulla la prima modalità attiva tra le seguenti: + +1. Lente d'ingrandimento +2. Dizionario +3. Barra Vai alla pagina +4. Schermo intero + +Se non è attiva alcuna modalità, il tasto Esc non esegue alcuna azione. + + + Disable mouse over activation Disabilita il mouse all'attivazione - + Scaling Ridimensionamento - + Scaling method Metodo di scala - + Nearest (fast, low quality) Più vicino (veloce, bassa qualità) - + Bilinear Bilineare - + Lanczos (better quality) Lanczos (qualità migliore) - + Page Flow Flusso pagine - + General Generale - + Brightness Luminosità - + Restart is needed Riavvio Necessario - + Fit options Opzioni di adattamento - + Enlarge images to fit width/height Ingrandisci le immagini per adattarle alla larghezza/altezza - + Double Page options Opzioni doppia pagina - + Show covers as single page Mostra le copertine come pagina singola @@ -704,48 +774,48 @@ Viewer - + Page not available! Pagina non disponibile! - - + + Press 'O' to open comic. Premi "O" per aprire il fumettto. - + Error opening comic Errore nell'apertura - + Cover! Copertina! - + CRC Error Errore CRC - + Comic not found Fumetto non trovato - + Not found Non trovato - + Last page! Ultima pagina! - + Loading...please wait! In caricamento...Attendi! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Apri - + Open a comic Apri un Fumetto - + New instance Nuova istanza - + Open Folder Apri una cartella - + Open image folder Apri la crettal immagini - + Open latest comic Apri l'ultimo fumetto - + Open the latest comic opened in the previous reading session Apri l'ultimo fumetto aperto nella sessione precedente - + Clear Cancella - + Clear open recent list Svuota la lista degli aperti - + Save Salva - - + + Save current page Salva la pagina corrente - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Fumetto precendente - - - + + + Open previous comic Apri il fumetto precendente - + Next Comic Prossimo fumetto - - - + + + Open next comic Apri il prossimo fumetto - + &Previous &Precedente - - - + + + Go to previous page Vai alla pagina precedente - + &Next &Prossimo - - - + + + Go to next page Vai alla prossima Pagina - + Fit Height Adatta altezza - + Fit image to height Adatta immagine all'altezza - + Fit Width Adatta Larghezza - + Fit image to width Adatta immagine in larghezza - + Show full size Mostra dimesioni reali - + Fit to page Adatta alla pagina - + Continuous scroll Scorrimento continuo - + Switch to continuous scroll mode Passa alla modalità di scorrimento continuo - + Reset zoom Resetta Zoom - + Show zoom slider Mostra cursore di zoom - + Zoom+ Aumenta - + Zoom- Riduci - + Rotate image to the left Ruota immagine a sinistra - + Rotate image to the right Ruota immagine a destra - + Double page mode Modalita doppia pagina - + Switch to double page mode Passa alla modalità doppia pagina - + Double page manga mode Modalità doppia pagina Manga - + Reverse reading order in double page mode Ordine lettura inverso in modo doppia pagina - + Go To Vai a - + Go to page ... Vai a Pagina ... - + Options Opzioni - + YACReader options Opzioni YACReader - - + + Help Aiuto - + Help, About YACReader Aiuto, crediti YACReader - + Magnifying glass Lente ingrandimento - + Switch Magnifying glass Passa a lente ingrandimento - + Set bookmark Imposta Segnalibro - + Set a bookmark on the current page Imposta segnalibro a pagina corrente - + Show bookmarks Mostra segnalibro - + Show the bookmarks of the current comic Mostra il segnalibro del fumetto corrente - + Show keyboard shortcuts Mostra scorciatoie da tastiera - + Show Info Mostra info - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Tasto Esc: esci o annulla la modalità attiva + + + Close Chiudi - + Show Dictionary Mostra dizionario - + Show go to flow Mostra "Vai a Comic Flow" - + Edit shortcuts Edita scorciatoie - + &File &Documento - - + + Open recent Apri i recenti - + File Documento - + Edit Edita - + View Mostra - + Go Vai - + Window Finestra - - - + Open Comic Apri Fumetto - - - + Comic files File Fumetto - + Open folder Apri cartella - - + + Comics Fumetto - + Toggle fullscreen mode Attiva/Disattiva schermo intero - + Hide/show toolbar Mostra/Nascondi Barra strumenti - - + + General Generale - + Size up magnifying glass Ingrandisci lente ingrandimento - + Size down magnifying glass Riduci lente ingrandimento - + Zoom in magnifying glass Ingrandisci in lente di ingrandimento - + Zoom out magnifying glass Riduci in lente di ingrandimento - + Reset magnifying glass Reimposta la lente d'ingrandimento - - + + Magnifiying glass Lente ingrandimento - + Toggle between fit to width and fit to height Passa tra adatta in larghezza ad altezza - - + + Page adjustement Correzioni di pagna - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Autoscorri Giù - + Autoscroll up Autoscorri Sù - + Autoscroll forward, horizontal first Autoscorri avanti, priorità Orizzontale - + Autoscroll backward, horizontal first Autoscorri indietro, priorità Orizzontale - + Autoscroll forward, vertical first Autoscorri avanti, priorità Verticale - + Autoscroll backward, vertical first Autoscorri indietro, priorità Verticale - + Move down Muovi Giù - + Move up Muovi Sù - + Move left Muovi Sinistra - + Move right Muovi Destra - + Go to the first page Vai alla pagina iniziale - + Go to the last page Vai all'ultima pagina - + Offset double page to the left Doppia pagina spostata a sinistra - + Offset double page to the right Doppia pagina spostata a destra - - + + Reading Leggi - + There is a new version available Nuova versione disponibile - + Do you want to download the new version? Vuoi scaricare la nuova versione? - + Remind me in 14 days Ricordamelo in 14 giorni - + Not now Non ora @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Salva - + Cancel Cancella - + Shortcuts Scorciatoia - + + Keyboard shortcuts + Scorciatoie da tastiera + + + + Customize the keyboard shortcuts used by the application. + Personalizza le scorciatoie da tastiera utilizzate dall'applicazione. + + + Edit shortcuts Edita Scorciatoia diff --git a/YACReader/yacreader_ko.ts b/YACReader/yacreader_ko.ts index 6fad3eb6d..475e4c299 100644 --- a/YACReader/yacreader_ko.ts +++ b/YACReader/yacreader_ko.ts @@ -92,24 +92,24 @@ 현재 테마 JSON을 불러올 수 없습니다. - + Import theme 테마 가져오기 - + JSON files (*.json);;All files (*) JSON 파일 (*.json);;모든 파일 (*) - + Could not import theme from: %1 다음에서 테마를 가져올 수 없습니다: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed 가져오기 실패 @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 페이지 %1 불러오는 중 @@ -188,25 +188,30 @@ FileComic - + 7z not found 7z를 찾을 수 없습니다 - + CRC error on page (%1): some of the pages will not be displayed correctly %1번 페이지에서 CRC 오류 발생: 일부 페이지가 올바르게 표시되지 않을 수 있습니다 - + Unknown error opening the file 파일을 여는 중 알 수 없는 오류가 발생했습니다 - + Format not supported 지원하지 않는 형식입니다 + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,20 +253,25 @@ HelpAboutDialog - + About 정보 - + Help 도움말 - + System info 시스템 정보 + + + Changelog + + OptionsDialog @@ -281,22 +291,22 @@ 시스템 기본값 - + Clear 지우기 - + General 일반 - + Appearance 외관 - + Options 환경설정 @@ -315,158 +325,218 @@ Show time in current page information label 현재 페이지 정보 라벨에 시간 표시 + + + Magnifying glass + 돋보기 + + Circular magnifying glass + 원형 돋보기 + + + + Draw a ring around the circular magnifying glass + 원형 돋보기 주위에 테두리 표시 + + + + Ease cursor movement toward the edges + 가장자리 쪽으로 커서 이동 완화 + + + "Go to flow" size 페이지 흐름 크기 - + Background color 배경색 - + Choose 선택 - + Scroll behaviour 스크롤 동작 - + Disable scroll animations and smooth scrolling 스크롤 애니메이션과 부드러운 스크롤 끄기 - + Do not turn page using scroll 스크롤로 페이지 넘기지 않기 - + Use single scroll step to turn page 한 단계 스크롤로 페이지 넘기기 - + Mouse mode 마우스 모드 - + Only Back/Forward buttons can turn pages 뒤로/앞으로 버튼만 페이지 넘김 - + Use the Left/Right buttons to turn pages. 왼쪽/오른쪽 버튼으로 페이지 넘김. - + Click left or right half of the screen to turn pages. 화면 왼쪽 또는 오른쪽 절반을 클릭하여 페이지 넘김. + + + Escape key + Esc 키 + + + + Quit the reader + 리더 종료 + + + + Cancel the active mode + 활성 모드 취소 + + Escape closes the reader, even while a mode is active. + 모드가 활성화되어 있어도 Esc 키를 누르면 리더가 종료됩니다. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Esc 키를 누르면 다음 중 활성화된 첫 번째 모드가 취소됩니다: + +1. 돋보기 +2. 사전 +3. 페이지 이동 표시줄 +4. 전체 화면 + +활성화된 모드가 없으면 Esc 키를 눌러도 아무 동작도 하지 않습니다. + + + Quick Navigation Mode 빠른 탐색 모드 - + Disable mouse over activation 마우스 오버 활성화 끄기 - + Brightness 밝기 - + Contrast 대비 - + Gamma 감마 - + Reset 초기화 - + Image options 이미지 옵션 - + Fit options 맞춤 옵션 - + Enlarge images to fit width/height 작은 그림도 꽉차게 보기 - + Double Page options 두 페이지 옵션 - + Show covers as single page 표지를 한 장으로 표시 - + Scaling 스케일링 - + Scaling method 스케일링 방법 - + Nearest (fast, low quality) 빠른 모드 (빠름, 저화질) - + Bilinear 보통 모드 (중간 품질) - + Lanczos (better quality) 고화질 모드 (더 좋은 화질) - + Page Flow 페이지 플로우 - + Image adjustment 이미지 조정 - + Restart is needed 재시작이 필요합니다 - + Comics directory 만화 폴더 @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. 'O'를 눌러 만화를 열어보세요. - + Not found 찾을 수 없음 - + Comic not found 만화를 찾을 수 없습니다 - + Error opening comic 만화를 여는 중 오류가 발생했습니다 - + CRC Error CRC 오류 - + Loading...please wait! 불러오는 중... 잠시 기다려주세요! - + Page not available! 페이지를 불러올 수 없습니다! - + Cover! 표지! - + Last page! 마지막 페이지! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open 열기(&O) - + Open a comic 만화 열기 - + New instance 새 창 - + Open Folder 폴더 열기 - + Open image folder 이미지 폴더 열기 - + Open latest comic 마지막 만화 열기 - + Open the latest comic opened in the previous reading session 이전 작업에서 마지막으로 열었던 만화 열기 - + Clear 지우기 - + Clear open recent list 최근 목록 지우기 - + Save 저장 - - + + Save current page 현재 페이지 저장 - - - - + + + + Extract page(s) - + - + Extract page(s) from the original source - + - + Previous Comic 이전 만화 - - - + + + Open previous comic 이전 만화 열기 - + Next Comic 다음 만화 - - - + + + Open next comic 다음 만화 열기 - + &Previous 이전(&P) - - - + + + Go to previous page 이전 페이지로 이동 - + &Next 다음(&N) - - - + + + Go to next page 다음 페이지로 이동 - + Fit Height 꽉차게 보기 (높이 맞춤) - + Fit image to height 이미지를 높이에 맞춤 - + Fit Width 꽉차게 보기 (폭 맞춤) - + Fit image to width 이미지를 폭에 맞춤 - + Show full size 원본 크기 (100%)로 보기 - + Fit to page 꽉차게 보기 - + Continuous scroll 연속 스크롤 - + Switch to continuous scroll mode 연속 스크롤 모드로 전환 - + Reset zoom 확대/축소 초기화 - + Show zoom slider 확대/축소 슬라이더 보기 - + Zoom+ 확대+ - + Zoom- 축소- - + Rotate image to the left 이미지 왼쪽으로 회전 - + Rotate image to the right 이미지 오른쪽으로 회전 - + Double page mode 두 페이지씩 보기 (왼쪽 → 오른쪽) - + Switch to double page mode 두 페이지씩 보기로 전환 - + Double page manga mode 두 페이지씩 보기 (왼쪽 ← 오른쪽) - + Reverse reading order in double page mode 두 페이지씩 보기에서 읽기 순서 뒤집기 - + Go To 이동 - + Go to page ... 페이지로 이동... - + Options 환경설정 - + YACReader options YACReader 환경설정 - - + + Help 도움말 - + Help, About YACReader 도움말, YACReader 정보 - + Magnifying glass 돋보기 - + Switch Magnifying glass 돋보기 전환 - + Set bookmark 책갈피 설정 - + Set a bookmark on the current page 현재 페이지에 책갈피 설정 - + Show bookmarks 책갈피 보기 - + Show the bookmarks of the current comic 현재 만화의 책갈피 보기 - + Show keyboard shortcuts 키보드 단축키 보기 - + Show Info 정보 보기 - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Esc 키: 종료 또는 활성 모드 취소 + + + Close 닫기 - + Show Dictionary 사전 보기 - + Show go to flow 페이지 흐름 보기 - + Edit shortcuts 단축키 편집 - + &File 파일(&F) - - + + Open recent 최근 항목 열기 - + File 파일 - + Edit 편집 - + View 보기 - + Go 이동 - + Window - - - + Open Comic 만화 열기 - - - + Comic files 만화 파일 - + Open folder 폴더 열기 - + Overwrite file? - + - + The file already exists. Do you want to overwrite it? - + - + The current page could not be extracted. - + - + Overwrite files? - + - + Some files already exist. Do you want to overwrite them? - + - + Some pages could not be extracted. - + - - + + Comics 만화 - - + + General 일반 - - + + Magnifiying glass 돋보기 - - + + Page adjustement 페이지 조정 - - + + Reading 읽기 - + Toggle fullscreen mode 전체화면 전환 - + Hide/show toolbar 도구 모음 표시/숨김 - + Size up magnifying glass 돋보기 크게 - + Size down magnifying glass 돋보기 작게 - + Zoom in magnifying glass 돋보기 확대 - + Zoom out magnifying glass 돋보기 축소 - + Reset magnifying glass 돋보기 초기화 - + Toggle between fit to width and fit to height 폭 맞춤 / 높이 맞춤 전환 - + Autoscroll down 아래로 자동 스크롤 - + Autoscroll up 위로 자동 스크롤 - + Autoscroll forward, horizontal first 세로 우선으로 정방향 자동 스크롤 - + Autoscroll backward, horizontal first 가로 우선으로 정방향 자동 스크롤 - + Autoscroll forward, vertical first 세로 우선으로 역방향 자동 스크롤 - + Autoscroll backward, vertical first 가로 우선으로 역방향 자동 스크롤 - + Move down 아래로 이동 - + Move up 위로 이동 - + Move left 왼쪽으로 이동 - + Move right 오른쪽으로 이동 - + Go to the first page 첫 페이지로 이동 - + Go to the last page 마지막 페이지로 이동 - + Offset double page to the left 두 페이지 왼쪽으로 이동 - + Offset double page to the right 두 페이지 오른쪽으로 이동 - + There is a new version available 새 버전을 내려받으시겠습니까? - + Do you want to download the new version? 새 버전을 내려받으시겠습니까? - + Remind me in 14 days 14일 후에 다시 알림 - + Not now 나중에 @@ -1417,14 +1493,14 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + - + Previous versions - + @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save 저장 - + Cancel 취소 - + + Keyboard shortcuts + 키보드 단축키 + + + + Customize the keyboard shortcuts used by the application. + 애플리케이션에서 사용하는 키보드 단축키를 사용자 지정합니다. + + + Edit shortcuts 단축키 편집 - + Shortcuts 단축키 diff --git a/YACReader/yacreader_nl.ts b/YACReader/yacreader_nl.ts index 0bd929714..17fd353fb 100644 --- a/YACReader/yacreader_nl.ts +++ b/YACReader/yacreader_nl.ts @@ -92,24 +92,24 @@ De huidige thema-JSON kan niet worden geladen. - + Import theme Thema importeren - + JSON files (*.json);;All files (*) JSON-bestanden (*.json);;Alle bestanden (*) - + Could not import theme from: %1 Kan thema niet importeren uit: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Importeren is mislukt @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Pagina laden %1 @@ -188,25 +188,30 @@ FileComic - + 7z not found 7Z Archiefbestand niet gevonden - + CRC error on page (%1): some of the pages will not be displayed correctly CRC-fout op pagina (%1): sommige pagina's worden niet correct weergegeven - + Unknown error opening the file Onbekende fout bij het openen van het bestand - + Format not supported Formaat niet ondersteund + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Hulp - + System info Systeeminformatie - + + Changelog + + + + About Over @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Gammawaarde - + Reset Standaardwaarden terugzetten @@ -281,62 +291,62 @@ Pad naar mijn strips - + Scaling Schalen - + Scaling method Schaalmethode - + Nearest (fast, low quality) Dichtstbijzijnde (snel, lage kwaliteit) - + Bilinear Bilineair - + Lanczos (better quality) Lanczos (betere kwaliteit) - + Image adjustment Beeldaanpassing - + "Go to flow" size Grootte van "Ga naar Comic Flow" - + Choose Kies - + Image options Afbeelding opties - + Contrast Contrastwaarde - + Appearance Verschijning - + Options Opties @@ -356,42 +366,42 @@ Standaard van het systeem - + Clear Duidelijk - + Comics directory Strips map - + Background color Achtergrondkleur - + Page Flow Omslagbrowser - + General Algemeen - + Brightness Helderheid - + Restart is needed Herstart is nodig - + Quick Navigation Mode Snelle navigatiemodus @@ -406,67 +416,127 @@ Toon de tijd in het informatielabel van de huidige pagina - + + Magnifying glass + Vergrootglas + + + + Circular magnifying glass + Rond vergrootglas + + + + Draw a ring around the circular magnifying glass + Een rand rond het ronde vergrootglas tekenen + + + + Ease cursor movement toward the edges + Cursorbeweging naar de randen versoepelen + + + Scroll behaviour Scrollgedrag - + Disable scroll animations and smooth scrolling Schakel scrollanimaties en soepel scrollen uit - + Do not turn page using scroll Sla de pagina niet om met scrollen - + Use single scroll step to turn page Gebruik een enkele scrollstap om de pagina om te slaan - + Mouse mode Muismodus - + Only Back/Forward buttons can turn pages Alleen de knoppen Terug/Vooruit kunnen pagina's omslaan - + Use the Left/Right buttons to turn pages. Gebruik de knoppen Links/Rechts om pagina's om te slaan. - + Click left or right half of the screen to turn pages. Klik op de linker- of rechterhelft van het scherm om pagina's om te slaan. - + + Escape key + Escape-toets + + + + Quit the reader + Reader afsluiten + + + + Cancel the active mode + Actieve modus annuleren + + + + Escape closes the reader, even while a mode is active. + Met de Escape-toets wordt de reader afgesloten, ook als er een modus actief is. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + De Escape-toets annuleert de eerste actieve modus in deze lijst: + +1. Vergrootglas +2. Woordenboek +3. Ga naar pagina-balk +4. Volledig scherm + +Als geen enkele modus actief is, doet de Escape-toets niets. + + + Disable mouse over activation Schakel muis-over-activering uit - + Fit options Pas opties - + Enlarge images to fit width/height Vergroot afbeeldingen zodat ze in de breedte/hoogte passen - + Double Page options Opties voor dubbele pagina's - + Show covers as single page Toon omslagen als enkele pagina @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. Druk 'O' om een strip te openen. - + Cover! Omslag! - + Comic not found Strip niet gevonden - + Not found Niet gevonden - + Last page! Laatste pagina! - + Loading...please wait! Inladen...even wachten! - + Error opening comic Fout bij openen strip - + CRC Error CRC-fout - + Page not available! Pagina niet beschikbaar! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Openen - + Open a comic Open een strip - + New instance Nieuw exemplaar - + Open Folder Map Openen - + Open image folder Open afbeeldings map - + Open latest comic Open de nieuwste strip - + Open the latest comic opened in the previous reading session Open de nieuwste strip die in de vorige leessessie is geopend - + Clear Duidelijk - + Clear open recent list Wis geopende recente lijst - + Save Bewaar - - + + Save current page Bewaren huidige pagina - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Vorige Strip - - - + + + Open previous comic Open de vorige strip - + Next Comic Volgende Strip - - - + + + Open next comic Open volgende strip - + &Previous &Vorige - - - + + + Go to previous page Ga naar de vorige pagina - + &Next &Volgende - - - + + + Go to next page Ga naar de volgende pagina - + Fit Height Geschikte hoogte - + Fit image to height Afbeelding aanpassen aan hoogte - + Fit Width Vensterbreedte aanpassen - + Fit image to width Afbeelding aanpassen aan breedte - + Show full size Volledig Scherm - + Fit to page Aanpassen aan pagina - + Continuous scroll Continu scrollen - + Switch to continuous scroll mode Schakel over naar de continue scrollmodus - + Reset zoom Zoom opnieuw instellen - + Show zoom slider Zoomschuifregelaar tonen - + Zoom+ Inzoomen - + Zoom- Uitzoomen - + Rotate image to the left Links omdraaien - + Rotate image to the right Rechts omdraaien - + Double page mode Dubbele bladzijde modus - + Switch to double page mode Naar dubbele bladzijde modus - + Double page manga mode Manga-modus met dubbele pagina - + Reverse reading order in double page mode Omgekeerde leesvolgorde in dubbele paginamodus - + Go To Ga Naar - + Go to page ... Ga naar bladzijde ... - + Options Opties - + YACReader options YACReader opties - - + + Help Hulp - + Help, About YACReader Help, Over YACReader - + Magnifying glass Vergrootglas - + Switch Magnifying glass Overschakelen naar Vergrootglas - + Set bookmark Bladwijzer instellen - + Set a bookmark on the current page Een bladwijzer toevoegen aan de huidige pagina - + Show bookmarks Bladwijzers weergeven - + Show the bookmarks of the current comic Toon de bladwijzers van de huidige strip - + Show keyboard shortcuts Toon de sneltoetsen - + Show Info Info tonen - + + Escape + Escape + + + + Escape key: quit, or cancel the active mode + Escape-toets: afsluiten of actieve modus annuleren + + + Close Sluiten - + Show Dictionary Woordenlijst weergeven - + Show go to flow "Ga naar Comic Flow" tonen - + Edit shortcuts Snelkoppelingen bewerken - + &File &Bestand - - + + Open recent Recent geopend - + File Bestand - + Edit Bewerken - + View Weergave - + Go Gaan - + Window Raam - - - + Open Comic Open een Strip - - - + Comic files Strip bestanden - + Open folder Open een Map - - + + Comics Strips - + Toggle fullscreen mode Schakel de modus Volledig scherm in - + Hide/show toolbar Werkbalk verbergen/tonen - - + + General Algemeen - + Size up magnifying glass Vergrootglas vergroten - + Size down magnifying glass Vergrootglas kleiner maken - + Zoom in magnifying glass Zoom in vergrootglas - + Zoom out magnifying glass Uitzoomen vergrootglas - + Reset magnifying glass Vergrootglas opnieuw instellen - - + + Magnifiying glass Vergrootglas - + Toggle between fit to width and fit to height Schakel tussen Aanpassen aan breedte en Aanpassen aan hoogte - - + + Page adjustement Pagina-aanpassing - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Automatisch naar beneden scrollen - + Autoscroll up Automatisch omhoog scrollen - + Autoscroll forward, horizontal first Automatisch vooruit scrollen, eerst horizontaal - + Autoscroll backward, horizontal first Automatisch achteruit scrollen, eerst horizontaal - + Autoscroll forward, vertical first Automatisch vooruit scrollen, eerst verticaal - + Autoscroll backward, vertical first Automatisch achteruit scrollen, eerst verticaal - + Move down Ga naar beneden - + Move up Ga omhoog - + Move left Ga naar links - + Move right Ga naar rechts - + Go to the first page Ga naar de eerste pagina - + Go to the last page Ga naar de laatste pagina - + Offset double page to the left Dubbele pagina naar links verschoven - + Offset double page to the right Offset dubbele pagina naar rechts - - + + Reading Lezing - + There is a new version available Er is een nieuwe versie beschikbaar - + Do you want to download the new version? Wilt u de nieuwe versie downloaden? - + Remind me in 14 days Herinner mij er over 14 dagen aan - + Not now Niet nu @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Bewaar - + Cancel Annuleren - + + Keyboard shortcuts + Sneltoetsen + + + + Customize the keyboard shortcuts used by the application. + Pas de sneltoetsen aan die door de toepassing worden gebruikt. + + + Edit shortcuts Snelkoppelingen bewerken - + Shortcuts Snelkoppelingen diff --git a/YACReader/yacreader_pt.ts b/YACReader/yacreader_pt.ts index 6c03f167c..2adf055d5 100644 --- a/YACReader/yacreader_pt.ts +++ b/YACReader/yacreader_pt.ts @@ -92,24 +92,24 @@ O tema atual JSON não pôde ser carregado. - + Import theme Importar tema - + JSON files (*.json);;All files (*) Arquivos JSON (*.json);;Todos os arquivos (*) - + Could not import theme from: %1 Não foi possível importar o tema de: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Falha na importação @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Carregando página %1 @@ -188,25 +188,30 @@ FileComic - + 7z not found 7z não encontrado - + CRC error on page (%1): some of the pages will not be displayed correctly Erro CRC na página (%1): algumas páginas não serão exibidas corretamente - + Unknown error opening the file Erro desconhecido ao abrir o arquivo - + Format not supported Formato não suportado + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Ajuda - + System info Informações do sistema - + + Changelog + + + + About Sobre @@ -271,17 +281,17 @@ Meu caminho de quadrinhos - + "Go to flow" size Tamanho de "Ir para Comic Flow" - + Appearance Aparência - + Options Opções @@ -301,17 +311,17 @@ Padrão do sistema - + Clear Claro - + Comics directory Diretório de quadrinhos - + Restart is needed Reiniciar é necessário @@ -326,147 +336,207 @@ Mostrar hora no rótulo de informações da página atual - + + Magnifying glass + Lupa + + + + Circular magnifying glass + Lupa circular + + + + Draw a ring around the circular magnifying glass + Desenhar um anel ao redor da lupa circular + + + + Ease cursor movement toward the edges + Suavizar o movimento do cursor em direção às bordas + + + Background color Cor de fundo - + Choose Escolher - + Scroll behaviour Comportamento de rolagem - + Disable scroll animations and smooth scrolling Desative animações de rolagem e rolagem suave - + Do not turn page using scroll Não vire a página usando scroll - + Use single scroll step to turn page Use uma única etapa de rolagem para virar a página - + Mouse mode Modo mouse - + Only Back/Forward buttons can turn pages Apenas os botões Voltar/Avançar podem virar páginas - + Use the Left/Right buttons to turn pages. Use os botões Esquerda/Direita para virar as páginas. - + Click left or right half of the screen to turn pages. Clique na metade esquerda ou direita da tela para virar as páginas. + + + Escape key + Tecla Escape + + + + Quit the reader + Sair do leitor + + + + Cancel the active mode + Cancelar o modo ativo + + Escape closes the reader, even while a mode is active. + A tecla Escape fecha o leitor, mesmo quando existe um modo ativo. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + A tecla Escape cancela o primeiro destes modos que estiver ativo: + +1. Lupa +2. Dicionário +3. Barra Ir para a página +4. Ecrã inteiro + +Se nenhum estiver ativo, a tecla Escape não faz nada. + + + Quick Navigation Mode Modo de navegação rápida - + Disable mouse over activation Desativar ativação do mouse sobre - + Brightness Brilho - + Contrast Contraste - + Gamma Gama - + Reset Reiniciar - + Image options Opções de imagem - + Fit options Opções de ajuste - + Enlarge images to fit width/height Amplie as imagens para caber na largura/altura - + Double Page options Opções de página dupla - + Show covers as single page Mostrar capas como página única - + Scaling Dimensionamento - + Scaling method Método de dimensionamento - + Nearest (fast, low quality) Mais próximo (rápido, baixa qualidade) - + Bilinear Interpola??o bilinear - + Lanczos (better quality) Lanczos (melhor qualidade) - + General Em geral - + Page Flow Fluxo de página - + Image adjustment Ajuste de imagem @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. Pressione 'O' para abrir um quadrinho. - + Loading...please wait! Carregando... por favor, aguarde! - + Not found Não encontrado - + Comic not found Quadrinho não encontrado - + Error opening comic Erro ao abrir quadrinho - + CRC Error Erro CRC - + Page not available! Página não disponível! - + Cover! Cobrir! - + Last page! Última página! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Abrir - + Open a comic Abrir um quadrinho - + New instance Nova instância - + Open Folder Abrir Pasta - + Open image folder Abra a pasta de imagens - + Open latest comic Abra o último quadrinho - + Open the latest comic opened in the previous reading session Abra o último quadrinho aberto na sessão de leitura anterior - + Clear Claro - + Clear open recent list Limpar lista recente aberta - + Save Salvar - - + + Save current page Salvar página atual - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Quadrinho Anterior - - - + + + Open previous comic Abrir quadrinho anterior - + Next Comic Próximo Quadrinho - - - + + + Open next comic Abrir próximo quadrinho - + &Previous A&nterior - - - + + + Go to previous page Ir para a página anterior - + &Next &Próxima - - - + + + Go to next page Ir para a próxima página - + Fit Height Ajustar Altura - + Fit image to height Ajustar imagem à altura - + Fit Width Ajustar à Largura - + Fit image to width Ajustar imagem à largura - + Show full size Mostrar tamanho grande - + Fit to page Ajustar à página - + Continuous scroll Rolagem contínua - + Switch to continuous scroll mode Mudar para o modo de rolagem contínua - + Reset zoom Redefinir zoom - + Show zoom slider Mostrar controle deslizante de zoom - + Zoom+ Ampliar - + Zoom- Reduzir - + Rotate image to the left Girar imagem à esquerda - + Rotate image to the right Girar imagem à direita - + Double page mode Modo dupla página - + Switch to double page mode Alternar para o modo dupla página - + Double page manga mode Modo mangá de página dupla - + Reverse reading order in double page mode Ordem de leitura inversa no modo de página dupla - + Go To Ir Para - + Go to page ... Ir para a página... - + Options Opções - + YACReader options Opções do YACReader - - + + Help Ajuda - + Help, About YACReader Ajuda, Sobre o YACReader - + Magnifying glass Lupa - + Switch Magnifying glass Alternar Lupa - + Set bookmark Definir marcador - + Set a bookmark on the current page Definir um marcador na página atual - + Show bookmarks Mostrar marcadores - + Show the bookmarks of the current comic Mostrar os marcadores do quadrinho atual - + Show keyboard shortcuts Mostrar teclas de atalhos - + Show Info Mostrar Informações - + + Escape + Escape + + + + Escape key: quit, or cancel the active mode + Tecla Escape: sair ou cancelar o modo ativo + + + Close Fechar - + Show Dictionary Mostrar dicionário - + Show go to flow Mostrar "Ir para Comic Flow" - + Edit shortcuts Editar atalhos - + &File &Arquivo - - + + Open recent Abrir recente - + File Arquivo - + Edit Editar - + View Visualizar - + Go Ir - + Window Janela - - - + Open Comic Abrir Quadrinho - - - + Comic files Arquivos de quadrinhos - + Open folder Abrir pasta - - + + Comics Quadrinhos - + Toggle fullscreen mode Alternar modo de tela cheia - + Hide/show toolbar Ocultar/mostrar barra de ferramentas - - + + General Em geral - + Size up magnifying glass Dimensione a lupa - + Size down magnifying glass Diminuir o tamanho da lupa - + Zoom in magnifying glass Zoom na lupa - + Zoom out magnifying glass Diminuir o zoom da lupa - + Reset magnifying glass Redefinir lupa - - + + Magnifiying glass Lupa - + Toggle between fit to width and fit to height Alternar entre ajustar à largura e ajustar à altura - - + + Page adjustement Ajuste de página - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Rolagem automática para baixo - + Autoscroll up Rolagem automática para cima - + Autoscroll forward, horizontal first Rolagem automática para frente, horizontal primeiro - + Autoscroll backward, horizontal first Rolagem automática para trás, horizontal primeiro - + Autoscroll forward, vertical first Rolagem automática para frente, vertical primeiro - + Autoscroll backward, vertical first Rolagem automática para trás, vertical primeiro - + Move down Mover para baixo - + Move up Subir - + Move left Mover para a esquerda - + Move right Mover para a direita - + Go to the first page Vá para a primeira página - + Go to the last page Ir para a última página - + Offset double page to the left Deslocar página dupla para a esquerda - + Offset double page to the right Deslocar página dupla para a direita - - + + Reading Leitura - + There is a new version available Há uma nova versão disponível - + Do you want to download the new version? Você deseja baixar a nova versão? - + Remind me in 14 days Lembre-me em 14 dias - + Not now Agora não @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Salvar - + Cancel Cancelar - + + Keyboard shortcuts + Atalhos de teclado + + + + Customize the keyboard shortcuts used by the application. + Personalize os atalhos de teclado utilizados pela aplicação. + + + Edit shortcuts Editar atalhos - + Shortcuts Atalhos diff --git a/YACReader/yacreader_ru.ts b/YACReader/yacreader_ru.ts index 7fabebd44..25966fa4b 100644 --- a/YACReader/yacreader_ru.ts +++ b/YACReader/yacreader_ru.ts @@ -92,24 +92,24 @@ Не удалось загрузить JSON текущей темы. - + Import theme Импортировать тему - + JSON files (*.json);;All files (*) Файлы JSON (*.json);;Все файлы (*) - + Could not import theme from: %1 Не удалось импортировать тему из: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed Импорт не удался @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 Загрузка страницы %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported Формат не поддерживается - + 7z not found 7z не найден - + Unknown error opening the file Неизвестная ошибка при открытии файла - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Ошибка контрольной суммы CRC на странице (%1): некоторые страницы будут отображаться неправильно @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Справка - + System info Информация о системе - + + Changelog + + + + About О программе @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Гамма - + Reset Вернуть к первоначальным значениям @@ -281,37 +291,37 @@ Папка комиксов - + Image adjustment Настройка изображения - + "Go to flow" size Размер "Перейти к Comic Flow" - + Choose Выбрать - + Image options Настройки изображения - + Contrast Контраст - + Appearance Появление - + Options Настройки @@ -331,17 +341,17 @@ Системный по умолчанию - + Clear Очистить - + Comics directory Папка комиксов - + Quick Navigation Mode Ползунок для быстрой навигации по страницам @@ -356,117 +366,177 @@ Показывать время в информационной метке текущей страницы - + + Magnifying glass + Увеличительное стекло + + + + Circular magnifying glass + Круглое увеличительное стекло + + + + Draw a ring around the circular magnifying glass + Рисовать ободок вокруг круглого увеличительного стекла + + + + Ease cursor movement toward the edges + Сглаживать движение курсора к краям + + + Background color Фоновый цвет - + Scroll behaviour Поведение прокрутки - + Disable scroll animations and smooth scrolling Отключить анимацию прокрутки и плавную прокрутку - + Do not turn page using scroll Не переворачивайте страницу с помощью прокрутки - + Use single scroll step to turn page Используйте один шаг прокрутки, чтобы перевернуть страницу - + Mouse mode Режим мыши - + Only Back/Forward buttons can turn pages Только кнопки «Назад/Вперед» могут перелистывать страницы. - + Use the Left/Right buttons to turn pages. Используйте кнопки «Влево/Вправо», чтобы перелистывать страницы. - + Click left or right half of the screen to turn pages. Нажмите левую или правую половину экрана, чтобы перелистывать страницы. - + + Escape key + Клавиша Esc + + + + Quit the reader + Выйти из программы чтения + + + + Cancel the active mode + Отменить активный режим + + + + Escape closes the reader, even while a mode is active. + Клавиша Esc закрывает программу чтения, даже если активен какой-либо режим. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Клавиша Esc отключает первый активный режим из списка: + +1. Лупа +2. Словарь +3. Панель перехода к странице +4. Полноэкранный режим + +Если ни один режим не активен, клавиша Esc ничего не делает. + + + Disable mouse over activation Отключить активацию потока при наведении мыши - + Scaling Масштабирование - + Scaling method Метод масштабирования - + Nearest (fast, low quality) Ближайший (быстро, низкое качество) - + Bilinear Билинейный - + Lanczos (better quality) Ланцос (лучшее качество) - + Page Flow Поток Страниц - + General Общие - + Brightness Яркость - + Restart is needed Требуется перезагрузка - + Fit options Варианты подгонки - + Enlarge images to fit width/height Увеличьте изображения по ширине/высоте - + Double Page options Параметры двойной страницы - + Show covers as single page Показывать обложки на одной странице @@ -704,48 +774,48 @@ Viewer - + Page not available! Страница недоступна! - - + + Press 'O' to open comic. Нажмите "O" чтобы открыть комикс. - + Error opening comic Ошибка открытия комикса - + Cover! Начало! - + CRC Error Ошибка CRC - + Comic not found Комикс не найден - + Not found Не найдено - + Last page! Конец! - + Loading...please wait! Загрузка... Пожалуйста подождите! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Открыть - + Open a comic Открыть комикс - + New instance Новый экземпляр - + Open Folder Открыть папку - + Open image folder Открыть папку с изображениями - + Open latest comic Открыть последний комикс - + Open the latest comic opened in the previous reading session Открыть комикс открытый в предыдущем сеансе чтения - + Clear Очистить - + Clear open recent list Очистить список недавно открытых файлов - + Save Сохранить - - + + Save current page Сохранить текущию страницу - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Предыдущий комикс - - - + + + Open previous comic Открыть предыдуший комикс - + Next Comic Следующий комикс - - - + + + Open next comic Открыть следующий комикс - + &Previous &Предыдущий - - - + + + Go to previous page Перейти к предыдущей странице - + &Next &Следующий - - - + + + Go to next page Перейти к следующей странице - + Fit Height Подогнать по высоте - + Fit image to height Подогнать по высоте - + Fit Width Подогнать по ширине - + Fit image to width Подогнать по ширине - + Show full size Показать в полном размере - + Fit to page Подогнать под размер страницы - + Continuous scroll Непрерывная прокрутка - + Switch to continuous scroll mode Переключиться в режим непрерывной прокрутки - + Reset zoom Сбросить масштаб - + Show zoom slider Показать ползунок масштабирования - + Zoom+ Увеличить масштаб - + Zoom- Уменьшить масштаб - + Rotate image to the left Повернуть изображение против часовой стрелки - + Rotate image to the right Повернуть изображение по часовой стрелке - + Double page mode Двухстраничный режим - + Switch to double page mode Двухстраничный режим - + Double page manga mode Двухстраничный режим манги - + Reverse reading order in double page mode Двухстраничный режим манги - + Go To Перейти к странице... - + Go to page ... Перейти к странице... - + Options Настройки - + YACReader options Настройки - - + + Help Справка - + Help, About YACReader Справка - + Magnifying glass Увеличительное стекло - + Switch Magnifying glass Увеличительное стекло - + Set bookmark Установить закладку - + Set a bookmark on the current page Установить закладку на текущей странице - + Show bookmarks Показать закладки - + Show the bookmarks of the current comic Показать закладки в текущем комиксе - + Show keyboard shortcuts Показать горячие клавиши - + Show Info Показать/скрыть номер страницы и текущее время - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Клавиша Esc: выход или отмена активного режима + + + Close Закрыть - + Show Dictionary Переводчик YACreader - + Show go to flow Показать "Перейти к Comic Flow" - + Edit shortcuts Редактировать горячие клавиши - + &File &Отображать панель инструментов - - + + Open recent Открыть недавние - + File Файл - + Edit Редактировать - + View Посмотреть - + Go Перейти - + Window Окно - - - + Open Comic Открыть комикс - - - + Comic files Файлы комикса - + Open folder Открыть папку - - + + Comics Комикс - + Toggle fullscreen mode Полноэкранный режим включить/выключить - + Hide/show toolbar Показать/скрыть панель инструментов - - + + General Общие - + Size up magnifying glass Увеличение размера окошка увеличительного стекла - + Size down magnifying glass Уменьшение размера окошка увеличительного стекла - + Zoom in magnifying glass Увеличить - + Zoom out magnifying glass Уменьшить - + Reset magnifying glass Сбросить увеличительное стекло - - + + Magnifiying glass Увеличительное стекло - + Toggle between fit to width and fit to height Переключение режима подгонки страницы по ширине/высоте - - + + Page adjustement Настройка страницы - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Автопрокрутка вниз - + Autoscroll up Автопрокрутка вверх - + Autoscroll forward, horizontal first Автопрокрутка вперед, горизонтальная - + Autoscroll backward, horizontal first Автопрокрутка назад, горизонтальная - + Autoscroll forward, vertical first Автопрокрутка вперед, вертикальная - + Autoscroll backward, vertical first Автопрокрутка назад, вертикальная - + Move down Переместить вниз - + Move up Переместить вверх - + Move left Переместить влево - + Move right Переместить вправо - + Go to the first page Перейти к первой странице - + Go to the last page Перейти к последней странице - + Offset double page to the left Смещение разворота влево - + Offset double page to the right Смещение разворота вправо - - + + Reading Чтение - + There is a new version available Доступна новая версия - + Do you want to download the new version? Хотите загрузить новую версию ? - + Remind me in 14 days Напомнить через 14 дней - + Not now Не сейчас @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Сохранить - + Cancel Отмена - + Shortcuts Горячие клавиши - + + Keyboard shortcuts + Сочетания клавиш + + + + Customize the keyboard shortcuts used by the application. + Настройте сочетания клавиш, используемые приложением. + + + Edit shortcuts Редактировать горячие клавиши diff --git a/YACReader/yacreader_source.ts b/YACReader/yacreader_source.ts index 70c17057f..b0f23d4a1 100644 --- a/YACReader/yacreader_source.ts +++ b/YACReader/yacreader_source.ts @@ -92,23 +92,23 @@ - + Import theme - + JSON files (*.json);;All files (*) - + Could not import theme from: %1 - + Could not import theme from: %1 @@ -116,7 +116,7 @@ - + Import failed @@ -148,7 +148,7 @@ ContinuousPageWidget - + Loading page %1 @@ -184,25 +184,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly - + Unknown error opening the file - + 7z not found - + Format not supported + + + Unsupported EPUB: %1 + + GoToDialog @@ -244,25 +249,30 @@ HelpAboutDialog - + About - + Help - + System info + + + Changelog + + OptionsDialog - + "Go to flow" size @@ -272,32 +282,32 @@ - + Background color - + Choose - + Quick Navigation Mode - + Disable mouse over activation - + Restart is needed - + Brightness @@ -327,142 +337,195 @@ - + + Magnifying glass + + + + + Circular magnifying glass + + + + + Draw a ring around the circular magnifying glass + + + + + Ease cursor movement toward the edges + + + + Clear - + Scroll behaviour - + Disable scroll animations and smooth scrolling - + Do not turn page using scroll - + Use single scroll step to turn page - + Mouse mode - + Only Back/Forward buttons can turn pages - + Use the Left/Right buttons to turn pages. - + Click left or right half of the screen to turn pages. - + + Escape key + + + + + Quit the reader + + + + + Cancel the active mode + + + + + Escape closes the reader, even while a mode is active. + + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + + + + Contrast - + Gamma - + Reset - + Image options - + Fit options - + Enlarge images to fit width/height - + Double Page options - + Show covers as single page - + Scaling - + Scaling method - + Nearest (fast, low quality) - + Bilinear - + Lanczos (better quality) - + General - + Page Flow - + Image adjustment - + Appearance - + Options - + Comics directory @@ -697,48 +760,48 @@ Viewer - - + + Press 'O' to open comic. - + Not found - + Comic not found - + Error opening comic - + CRC Error - + Loading...please wait! - + Page not available! - + Cover! - + Last page! @@ -864,545 +927,551 @@ YACReader::MainWindowViewer - + &Open - + Open a comic - + New instance - + Open Folder - + Open image folder - + Open latest comic - + Open the latest comic opened in the previous reading session - + Clear - + Clear open recent list - + Save - - + + Save current page - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic - - - + + + Open previous comic - + Next Comic - - - + + + Open next comic - + &Previous - - - + + + Go to previous page - + &Next - - - + + + Go to next page - + Fit Height - + Fit image to height - + Fit Width - + Fit image to width - + Show full size - + Fit to page - + Continuous scroll - + Switch to continuous scroll mode - + Reset zoom - + Show zoom slider - + Zoom+ - + Zoom- - + Rotate image to the left - + Rotate image to the right - + Double page mode - + Switch to double page mode - + Double page manga mode - + Reverse reading order in double page mode - + Go To - + Go to page ... - + Options - + YACReader options - - + + Help - + Help, About YACReader - + Magnifying glass - + Switch Magnifying glass - + Set bookmark - + Set a bookmark on the current page - + Show bookmarks - + Show the bookmarks of the current comic - + Show keyboard shortcuts - + Show Info - + + Escape + + + + + Escape key: quit, or cancel the active mode + + + + Close - + Show Dictionary - + Show go to flow - + Edit shortcuts - + &File - - + + Open recent - + File - + Edit - + View - + Go - + Window - - - + Open Comic - - - + Comic files - + Open folder - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - - + + Comics - - + + General - - + + Magnifiying glass - - + + Page adjustement - - + + Reading - + Toggle fullscreen mode - + Hide/show toolbar - + Size up magnifying glass - + Size down magnifying glass - + Zoom in magnifying glass - + Zoom out magnifying glass - + Reset magnifying glass - + Toggle between fit to width and fit to height - + Autoscroll down - + Autoscroll up - + Autoscroll forward, horizontal first - + Autoscroll backward, horizontal first - + Autoscroll forward, vertical first - + Autoscroll backward, vertical first - + Move down - + Move up - + Move left - + Move right - + Go to the first page - + Go to the last page - + Offset double page to the left - + Offset double page to the right - + There is a new version available - + Do you want to download the new version? - + Remind me in 14 days - + Not now @@ -1410,12 +1479,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1453,22 +1522,32 @@ YACReaderOptionsDialog - + Save - + Cancel - + + Keyboard shortcuts + + + + + Customize the keyboard shortcuts used by the application. + + + + Edit shortcuts - + Shortcuts diff --git a/YACReader/yacreader_tr.ts b/YACReader/yacreader_tr.ts index 203d49d69..d397cdd45 100644 --- a/YACReader/yacreader_tr.ts +++ b/YACReader/yacreader_tr.ts @@ -92,24 +92,24 @@ Geçerli tema JSON yüklenemedi. - + Import theme Temayı içe aktar - + JSON files (*.json);;All files (*) JSON dosyaları (*.json);;Tüm dosyalar (*) - + Could not import theme from: %1 Tema şu kaynaktan içe aktarılamadı: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed İçe aktarma başarısız oldu @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 %1 sayfası yükleniyor @@ -188,25 +188,30 @@ FileComic - + 7z not found 7z bulunamadı - + CRC error on page (%1): some of the pages will not be displayed correctly (%1). sayfada CRC hatası : bazı sayfalar düzgün görüntülenmeyecek - + Unknown error opening the file Dosya açılırken bilinmeyen hata - + Format not supported Biçim desteklenmiyor + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,17 +253,22 @@ HelpAboutDialog - + Help Yardım - + System info Sistem bilgisi - + + Changelog + + + + About Hakkında @@ -266,12 +276,12 @@ OptionsDialog - + Gamma Gama - + Reset Yeniden başlat @@ -281,62 +291,62 @@ Çizgi Romanlarım - + Scaling Ölçeklendirme - + Scaling method Ölçeklendirme yöntemi - + Nearest (fast, low quality) En yakın (hızlı, düşük kalite) - + Bilinear Çift doğrusal - + Lanczos (better quality) Lanczos (daha kaliteli) - + Image adjustment Resim ayarları - + "Go to flow" size "Comic Flow'a git" boyutu - + Choose Seç - + Image options Sayfa ayarları - + Contrast Kontrast - + Appearance Dış görünüş - + Options Ayarlar @@ -356,42 +366,42 @@ Sistem varsayılanı - + Clear Temizle - + Comics directory Çizgi roman konumu - + Background color Arka plan rengi - + Page Flow Sayfa akışı - + General Genel - + Brightness Parlaklık - + Restart is needed Yeniden başlatılmalı - + Quick Navigation Mode Hızlı Gezinti Kipi @@ -406,67 +416,127 @@ Geçerli sayfa bilgisi etiketinde zamanı göster - + + Magnifying glass + Büyüteç + + + + Circular magnifying glass + Dairesel büyüteç + + + + Draw a ring around the circular magnifying glass + Dairesel büyütecin etrafına halka çiz + + + + Ease cursor movement toward the edges + İmlecin kenarlara doğru hareketini yumuşat + + + Scroll behaviour Kaydırma davranışı - + Disable scroll animations and smooth scrolling Kaydırma animasyonlarını ve düzgün kaydırmayı devre dışı bırakın - + Do not turn page using scroll Kaydırmayı kullanarak sayfayı çevirmeyin - + Use single scroll step to turn page Sayfayı çevirmek için tek kaydırma adımını kullanın - + Mouse mode Fare modu - + Only Back/Forward buttons can turn pages Yalnızca Geri/İleri düğmeleri sayfaları çevirebilir - + Use the Left/Right buttons to turn pages. Sayfaları çevirmek için Sol/Sağ tuşlarını kullanın. - + Click left or right half of the screen to turn pages. Sayfaları çevirmek için ekranın sol veya sağ yarısına tıklayın. - + + Escape key + Escape tuşu + + + + Quit the reader + Okuyucudan çık + + + + Cancel the active mode + Etkin modu iptal et + + + + Escape closes the reader, even while a mode is active. + Bir mod etkinken bile Escape tuşu okuyucuyu kapatır. + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + Escape tuşu, aşağıdakilerden etkin olan ilkini iptal eder: + +1. Büyüteç +2. Sözlük +3. Sayfaya git çubuğu +4. Tam ekran + +Hiçbiri etkin değilse Escape tuşu hiçbir şey yapmaz. + + + Disable mouse over activation Etkinleştirme üzerinde fareyi devre dışı bırak - + Fit options Sığdırma seçenekleri - + Enlarge images to fit width/height Genişliğe/yüksekliği sığmaları için resimleri genişlet - + Double Page options Çift Sayfa seçenekleri - + Show covers as single page Kapakları tek sayfa olarak göster @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. 'O'ya basarak aç. - + Cover! Kapak! - + Comic not found Çizgi roman bulunamadı - + Not found Bulunamadı - + Last page! Son sayfa! - + Loading...please wait! Yükleniyor... lütfen bekleyin! - + Error opening comic Çizgi roman açılırken hata - + CRC Error CRC Hatası - + Page not available! Sayfa bulunamadı! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open &Aç - + Open a comic Çizgi romanı aç - + New instance Yeni örnek - + Open Folder Dosyayı Aç - + Open image folder Resim dosyasınıaç - + Open latest comic En son çizgi romanı aç - + Open the latest comic opened in the previous reading session Önceki okuma oturumunda açılan en son çizgi romanı aç - + Clear Temizle - + Clear open recent list Son açılanlar listesini temizle - + Save Kaydet - - + + Save current page Geçerli sayfayı kaydet - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic Önce ki çizgi roman - - - + + + Open previous comic Önceki çizgi romanı aç - + Next Comic Sırada ki çizgi roman - - - + + + Open next comic Sıradaki çizgi romanı aç - + &Previous &Geri - - - + + + Go to previous page Önceki sayfaya dön - + &Next &İleri - - - + + + Go to next page Sonra ki sayfaya geç - + Fit Height Yüksekliğe Sığdır - + Fit image to height Uygun yüksekliğe getir - + Fit Width Uygun Genişlik - + Fit image to width Görüntüyü sığdır - + Show full size Tam erken - + Fit to page Sayfaya sığdır - + Continuous scroll Sürekli kaydırma - + Switch to continuous scroll mode Sürekli kaydırma moduna geç - + Reset zoom Yakınlaştırmayı sıfırla - + Show zoom slider Yakınlaştırma çubuğunu göster - + Zoom+ Yakınlaştır - + Zoom- Uzaklaştır - + Rotate image to the left Sayfayı sola yatır - + Rotate image to the right Sayfayı sağa yator - + Double page mode Çift sayfa modu - + Switch to double page mode Çift sayfa moduna geç - + Double page manga mode Çift sayfa manga kipi - + Reverse reading order in double page mode Çift sayfa kipinde ters okuma sırası - + Go To Git - + Go to page ... Sayfata git... - + Options Ayarlar - + YACReader options YACReader ayarları - - + + Help Yardım - + Help, About YACReader YACReader hakkında yardım ve bilgi - + Magnifying glass Büyüteç - + Switch Magnifying glass Büyüteç - + Set bookmark Yer imi yap - + Set a bookmark on the current page Sayfayı yer imi olarak ayarla - + Show bookmarks Yer imlerini göster - + Show the bookmarks of the current comic Bu çizgi romanın yer imlerini göster - + Show keyboard shortcuts Klavye kısayollarını göster - + Show Info Bilgiyi göster - + + Escape + Escape + + + + Escape key: quit, or cancel the active mode + Escape tuşu: çık veya etkin modu iptal et + + + Close Kapat - + Show Dictionary Sözlüğü göster - + Show go to flow "Comic Flow'a git"i göster - + Edit shortcuts Kısayolları düzenle - + &File &Dosya - - + + Open recent Son dosyaları aç - + File Dosya - + Edit Düzen - + View Görünüm - + Go Git - + Window Pencere - - - + Open Comic Çizgi Romanı Aç - - - + Comic files Çizgi Roman Dosyaları - + Open folder Dosyayı aç - - + + Comics Çizgi Roman - + Toggle fullscreen mode Tam ekran kipini aç/kapat - + Hide/show toolbar Araç çubuğunu göster/gizle - - + + General Genel - + Size up magnifying glass Büyüteci büyüt - + Size down magnifying glass Büyüteci küçült - + Zoom in magnifying glass Büyüteci yakınlaştır - + Zoom out magnifying glass Büyüteci uzaklaştır - + Reset magnifying glass Büyüteci sıfırla - - + + Magnifiying glass Büyüteç - + Toggle between fit to width and fit to height Genişliğe sığdır ile yüksekliğe sığdır arasında geçiş yap - - + + Page adjustement Sayfa ayarı - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down Otomatik aşağı kaydır - + Autoscroll up Otomatik yukarı kaydır - + Autoscroll forward, horizontal first Otomatik ileri kaydır, önce yatay - + Autoscroll backward, horizontal first Otomatik geri kaydır, önce yatay - + Autoscroll forward, vertical first Otomatik ileri kaydır, önce dikey - + Autoscroll backward, vertical first Otomatik geri kaydır, önce dikey - + Move down Aşağı git - + Move up Yukarı git - + Move left Sola git - + Move right Sağa git - + Go to the first page İlk sayfaya git - + Go to the last page En son sayfaya git - + Offset double page to the left Çift sayfayı sola kaydır - + Offset double page to the right Çift sayfayı sağa kaydır - - + + Reading Okuma - + There is a new version available Yeni versiyon mevcut - + Do you want to download the new version? Yeni versiyonu indirmek ister misin ? - + Remind me in 14 days 14 gün içinde hatırlat - + Not now Şimdi değil @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save Kaydet - + Cancel Vazgeç - + + Keyboard shortcuts + Klavye kısayolları + + + + Customize the keyboard shortcuts used by the application. + Uygulama tarafından kullanılan klavye kısayollarını özelleştirin. + + + Edit shortcuts Kısayolları düzenle - + Shortcuts Kısayollar diff --git a/YACReader/yacreader_zh_CN.ts b/YACReader/yacreader_zh_CN.ts index eb6ae41b4..0a717df42 100644 --- a/YACReader/yacreader_zh_CN.ts +++ b/YACReader/yacreader_zh_CN.ts @@ -24,17 +24,17 @@ Light - 亮度 + 明亮 Dark - 黑暗的 + 暗黑 Custom - 风俗 + 自定义 @@ -49,12 +49,12 @@ Light: - 光: + 明亮: Dark: - 黑暗的: + 暗黑: @@ -92,24 +92,24 @@ 无法加载当前主题 JSON。 - + Import theme 导入主题 - + JSON files (*.json);;All files (*) JSON 文件 (*.json);;所有文件 (*) - + Could not import theme from: %1 无法从以下位置导入主题: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed 导入失败 @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 正在加载页面 %1 @@ -188,22 +188,27 @@ FileComic - + Format not supported 不支持的文件格式 - + 7z not found 未找到 7z - + Unknown error opening the file 打开文件时出现未知错误 - + + Unsupported EPUB: %1 + 不支持的 EPUB 格式:%1 + + + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 页 CRC 校验失败: 部分页面将无法正确显示 @@ -248,45 +253,50 @@ HelpAboutDialog - + Help 帮助 - + About 关于 - + System info 系统信息 + + + Changelog + 更新日志 + OptionsDialog - + Gamma Gamma值 - + Reset 重置 - + Enlarge images to fit width/height 放大图片以适应宽度/高度 - + Disable scroll animations and smooth scrolling 禁用滚动动画和平滑滚动 - + Use single scroll step to turn page 使用单滚动步骤翻页 @@ -296,37 +306,37 @@ 我的漫画路径 - + Image adjustment 图像调整 - + "Go to flow" size - “转到 Comic Flow”大小 + “转到页面流”大小 - + Choose 选择 - + Show covers as single page 显示封面为单页 - + Do not turn page using scroll 滚动时不翻页 - + Fit options 适应项 - + Image options 图片选项 @@ -341,37 +351,97 @@ 在当前页面信息标签中显示时间 - + + Magnifying glass + 放大镜 + + + + Circular magnifying glass + 圆形放大镜 + + + + Draw a ring around the circular magnifying glass + 在圆形放大镜周围绘制边框 + + + + Ease cursor movement toward the edges + 平滑光标移向边缘的移动 + + + Mouse mode 鼠标模式 - + Only Back/Forward buttons can turn pages 只有后退/前进按钮可以翻页 - + Use the Left/Right buttons to turn pages. 使用向左/向右按钮翻页。 - + Click left or right half of the screen to turn pages. 单击屏幕的左半部分或右半部分即可翻页。 - + + Escape key + Esc 键 + + + + Quit the reader + 退出阅读器 + + + + Cancel the active mode + 取消当前模式 + + + + Escape closes the reader, even while a mode is active. + 即使有模式处于活动状态,按 Esc 键也会关闭阅读器。 + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + 按 Esc 键会取消以下第一个处于活动状态的模式: + +1. 放大镜 +2. 字典 +3. 跳转到页面栏 +4. 全屏 + +如果没有活动模式,按 Esc 键不会执行任何操作。 + + + Contrast 对比度 - + Appearance - 外貌 + 外观 - + Options 选项 @@ -391,82 +461,82 @@ 系统默认 - + Clear 清空 - + Comics directory 漫画目录 - + Quick Navigation Mode 快速导航模式 - + Background color 背景颜色 - + Double Page options 双页选项 - + Scroll behaviour 滚动效果 - + Disable mouse over activation 禁用鼠标激活 - + Scaling 缩放 - + Scaling method 缩放方法 - + Nearest (fast, low quality) 最近(快速,低质量) - + Bilinear 双线性 - + Lanczos (better quality) Lanczos(质量更好) - + Page Flow 页面流 - + General 常规 - + Brightness 亮度 - + Restart is needed 需要重启 @@ -549,12 +619,12 @@ Light - 亮度 + 明亮 Dark - 黑暗的 + 暗黑 @@ -569,7 +639,7 @@ Variant: - 变体: + 颜色设置: @@ -584,7 +654,7 @@ Value - 价值 + @@ -619,7 +689,7 @@ true - 真的 + true @@ -627,7 +697,7 @@ false - 错误的 + false @@ -704,48 +774,48 @@ Viewer - + Page not available! 页面不可用! - - + + Press 'O' to open comic. 按下 'O' 以打开漫画. - + Error opening comic 打开漫画时发生错误 - + Cover! 封面! - + CRC Error CRC 校验失败 - + Comic not found 未找到漫画 - + Not found 未找到 - + Last page! 尾页! - + Loading...please wait! 载入中... 请稍候! @@ -840,7 +910,7 @@ Light - 亮度 + 明亮 @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + Go 转到 - + Edit 编辑 - + File 文件 - - + + Help 帮助 - + Save 保存 - + View 查看 - + &File 文件(&F) - + &Next 下一页(&N) - + &Open 打开(&O) - + Clear 清空 - + Close 关闭 - - - + Open Comic 打开漫画 - + Go To 跳转 - + Zoom+ 放大 - + Zoom- 缩小 - + Open image folder 打开图片文件夹 - + Size down magnifying glass 减小放大镜尺寸 - + Zoom out magnifying glass 减小缩放级别 - + New instance 新建实例 - + Open latest comic 打开最近的漫画 - + Autoscroll up 向上自动滚动 - + Set bookmark 设置书签 - + Autoscroll forward, vertical first 向前自动滚动,垂直优先 - + Switch to double page mode 切换至双页模式 - - + + Save current page 保存当前页面 - + Size up magnifying glass 增大放大镜尺寸 - + Double page mode 双页模式 - + Move up 向上移动 - + Switch Magnifying glass 切换放大镜 - + Open Folder 打开文件夹 - - + + Comics 漫画 - + Offset double page to the right 双页向右偏移 - + Fit Height 适应高度 - + Autoscroll backward, vertical first 向后自动滚动,垂直优先 - - - + Comic files 漫画文件 - + Not now 现在不 - + Go to the first page 转到第一页 - - - + + + Go to previous page 转至上一页 - + Window 窗口 - + Open the latest comic opened in the previous reading session 打开最近阅读漫画 - + Open a comic 打开漫画 - + Next Comic 下一个漫画 - + Fit Width 适合宽度 - + Options 选项 - + Show Info 显示信息 - + Open folder 打开文件夹 - + Go to page ... 跳转至页面 ... - - + + Magnifiying glass 放大镜 - + Fit image to width 缩放图片以适应宽度 - + Toggle fullscreen mode 切换全屏模式 - + Toggle between fit to width and fit to height 切换显示为"适应宽度"或"适应高度" - + Move right 向右移动 - + Zoom in magnifying glass 增大缩放级别 - - + + Open recent 最近打开的文件 - + Offset double page to the left 双页向左偏移 - - + + Reading 阅读 - + &Previous 上一页(&P) - + Autoscroll forward, horizontal first 向前自动滚动,水平优先 - - - + + + Go to next page 转至下一页 - + Show keyboard shortcuts 显示键盘快捷键 - + Double page manga mode - 双页漫画模式 + 双页日漫模式 - + There is a new version available 有新版本可用 - + Autoscroll down 向下自动滚动 - - - + + + Open next comic 打开下一个漫画 - + Remind me in 14 days 14天后提醒我 - + Fit to page 适应页面 - + Show bookmarks 显示书签 - - - + + + Open previous comic 打开上一个漫画 - + Rotate image to the left 向左旋转图片 - + Fit image to height 缩放图片以适应高度 - - - - + + + + Extract page(s) - + 提取页面 - + Extract page(s) from the original source - + 从原始来源提取页面 - + Continuous scroll 连续滚动 - + Switch to continuous scroll mode 切换到连续滚动模式 - + Reset zoom 重置缩放 - + Show the bookmarks of the current comic 显示当前漫画的书签 - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Esc 键:退出或取消当前模式 + + + Show Dictionary 显示字典 - + Overwrite file? - + 覆盖文件? - + The file already exists. Do you want to overwrite it? - + 文件已存在。是​​否要覆盖它? - + The current page could not be extracted. - + 无法提取当前页面。 - + Overwrite files? - + 覆盖文件? - + Some files already exist. Do you want to overwrite them? - + 部分文件已存在。是​​否要覆盖它们? - + Some pages could not be extracted. - + 部分页面无法提取。 - + Reset magnifying glass 重置放大镜 - + Move down 向下移动 - + Move left 向左移动 - + Reverse reading order in double page mode 双页模式 (逆序阅读) - + YACReader options YACReader 选项 - + Clear open recent list 清空最近访问列表 - + Help, About YACReader 帮助, 关于 YACReader - + Show go to flow - 显示“转到 Comic Flow” + 显示转到页面流 - + Previous Comic 上一个漫画 - + Show full size 显示全尺寸 - + Hide/show toolbar 隐藏/显示 工具栏 - + Magnifying glass 放大镜 - + Edit shortcuts 编辑快捷键 - - + + General 常规 - + Set a bookmark on the current page 在当前页面设置书签 - - + + Page adjustement 页面调整 - + Show zoom slider 显示缩放滑块 - + Go to the last page 转到最后一页 - + Do you want to download the new version? 你要下载新版本吗? - + Rotate image to the right 向右旋转图片 - + Autoscroll backward, horizontal first 向后自动滚动,水平优先 @@ -1417,14 +1493,14 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + 暂无发行说明。 - + Previous versions - + 先前版本 @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + Shortcuts 快捷键 - + + Keyboard shortcuts + 键盘快捷键 + + + + Customize the keyboard shortcuts used by the application. + 自定义应用程序使用的键盘快捷键。 + + + Edit shortcuts 编辑快捷键 diff --git a/YACReader/yacreader_zh_HK.ts b/YACReader/yacreader_zh_HK.ts index 601cf73a3..67c895019 100644 --- a/YACReader/yacreader_zh_HK.ts +++ b/YACReader/yacreader_zh_HK.ts @@ -92,24 +92,24 @@ 無法載入目前主題 JSON。 - + Import theme 導入主題 - + JSON files (*.json);;All files (*) JSON 檔案 (*.json);;所有檔案 (*) - + Could not import theme from: %1 無法從以下位置匯入主題: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed 導入失敗 @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 正在載入頁面 %1 @@ -188,25 +188,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,25 +253,30 @@ HelpAboutDialog - + About 關於 - + Help 幫助 - + System info 系統資訊 + + + Changelog + + OptionsDialog - + "Go to flow" size 「前往 Comic Flow」大小 @@ -276,57 +286,57 @@ 我的漫畫路徑 - + Background color 背景顏色 - + Choose 選擇 - + Quick Navigation Mode 快速導航模式 - + Disable mouse over activation 禁用滑鼠啟動 - + Scaling 縮放 - + Scaling method 縮放方法 - + Nearest (fast, low quality) 最近(快速,低品質) - + Bilinear 雙線性 - + Lanczos (better quality) Lanczos(品質更好) - + Restart is needed 需要重啟 - + Brightness 亮度 @@ -341,92 +351,152 @@ 在目前頁面資訊標籤中顯示時間 - + + Magnifying glass + 放大鏡 + + + + Circular magnifying glass + 圓形放大鏡 + + + + Draw a ring around the circular magnifying glass + 在圓形放大鏡周圍繪製邊框 + + + + Ease cursor movement toward the edges + 平滑游標移向邊緣的移動 + + + Scroll behaviour 滾動效果 - + Disable scroll animations and smooth scrolling 停用滾動動畫和平滑滾動 - + Do not turn page using scroll 滾動時不翻頁 - + Use single scroll step to turn page 使用單滾動步驟翻頁 - + Mouse mode 滑鼠模式 - + Only Back/Forward buttons can turn pages 只有後退/前進按鈕可以翻頁 - + Use the Left/Right buttons to turn pages. 使用向左/向右按鈕翻頁。 - + Click left or right half of the screen to turn pages. 點擊螢幕的左半部或右半部即可翻頁。 - + + Escape key + Esc 鍵 + + + + Quit the reader + 退出閱讀器 + + + + Cancel the active mode + 取消目前模式 + + + + Escape closes the reader, even while a mode is active. + 即使有模式正在使用,按 Esc 鍵仍會關閉閱讀器。 + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + 按 Esc 鍵會取消下列第一個正在使用的模式: + +1. 放大鏡 +2. 字典 +3. 前往頁面列 +4. 全螢幕 + +若沒有任何模式正在使用,按 Esc 鍵不會執行任何操作。 + + + Contrast 對比度 - + Gamma Gamma值 - + Reset 重置 - + Image options 圖片選項 - + Fit options 適應項 - + Enlarge images to fit width/height 放大圖片以適應寬度/高度 - + Double Page options 雙頁選項 - + Show covers as single page 顯示封面為單頁 - + General 常規 - + Appearance 外貌 @@ -446,27 +516,27 @@ 系統預設 - + Clear 清空 - + Page Flow 頁面流 - + Image adjustment 圖像調整 - + Options 選項 - + Comics directory 漫畫目錄 @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. 按下 'O' 以打開漫畫. - + Not found 未找到 - + Comic not found 未找到漫畫 - + Error opening comic 打開漫畫時發生錯誤 - + CRC Error CRC 校驗失敗 - + Loading...please wait! 載入中... 請稍候! - + Page not available! 頁面不可用! - + Cover! 封面! - + Last page! 尾頁! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open 打開(&O) - + Open a comic 打開漫畫 - + New instance 新建實例 - + Open Folder 打開檔夾 - + Open image folder 打開圖片檔夾 - + Open latest comic 打開最近的漫畫 - + Open the latest comic opened in the previous reading session 打開最近閱讀漫畫 - + Clear 清空 - + Clear open recent list 清空最近訪問列表 - + Save 保存 - - + + Save current page 保存當前頁面 - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic 上一個漫畫 - - - + + + Open previous comic 打開上一個漫畫 - + Next Comic 下一個漫畫 - - - + + + Open next comic 打開下一個漫畫 - + &Previous 上一頁(&P) - - - + + + Go to previous page 轉至上一頁 - + &Next 下一頁(&N) - - - + + + Go to next page 轉至下一頁 - + Fit Height 適應高度 - + Fit image to height 縮放圖片以適應高度 - + Fit Width 適合寬度 - + Fit image to width 縮放圖片以適應寬度 - + Show full size 顯示全尺寸 - + Fit to page 適應頁面 - + Continuous scroll 連續滾動 - + Switch to continuous scroll mode 切換到連續滾動模式 - + Reset zoom 重置縮放 - + Show zoom slider 顯示縮放滑塊 - + Zoom+ 放大 - + Zoom- 縮小 - + Rotate image to the left 向左旋轉圖片 - + Rotate image to the right 向右旋轉圖片 - + Double page mode 雙頁模式 - + Switch to double page mode 切換至雙頁模式 - + Double page manga mode 雙頁漫畫模式 - + Reverse reading order in double page mode 雙頁模式 (逆序閱讀) - + Go To 跳轉 - + Go to page ... 跳轉至頁面 ... - + Options 選項 - + YACReader options YACReader 選項 - - + + Help 幫助 - + Help, About YACReader 幫助, 關於 YACReader - + Magnifying glass 放大鏡 - + Switch Magnifying glass 切換放大鏡 - + Set bookmark 設置書簽 - + Set a bookmark on the current page 在當前頁面設置書簽 - + Show bookmarks 顯示書簽 - + Show the bookmarks of the current comic 顯示當前漫畫的書簽 - + Show keyboard shortcuts 顯示鍵盤快捷鍵 - + Show Info 顯示資訊 - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Esc 鍵:退出或取消目前模式 + + + Close 關閉 - + Show Dictionary 顯示字典 - + Show go to flow 顯示「前往 Comic Flow」 - + Edit shortcuts 編輯快捷鍵 - + &File 檔(&F) - - + + Open recent 最近打開的檔 - + File - + Edit 編輯 - + View 查看 - + Go 轉到 - + Window 窗口 - - - + Open Comic 打開漫畫 - - - + Comic files 漫畫檔 - + Open folder 打開檔夾 - - + + Comics 漫畫 - + Toggle fullscreen mode 切換全屏模式 - + Hide/show toolbar 隱藏/顯示 工具欄 - - + + General 常規 - + Size up magnifying glass 增大放大鏡尺寸 - + Size down magnifying glass 減小放大鏡尺寸 - + Zoom in magnifying glass 增大縮放級別 - + Zoom out magnifying glass 減小縮放級別 - + Reset magnifying glass 重置放大鏡 - - + + Magnifiying glass 放大鏡 - + Toggle between fit to width and fit to height 切換顯示為"適應寬度"或"適應高度" - - + + Page adjustement 頁面調整 - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down 向下自動滾動 - + Autoscroll up 向上自動滾動 - + Autoscroll forward, horizontal first 向前自動滾動,水準優先 - + Autoscroll backward, horizontal first 向後自動滾動,水準優先 - + Autoscroll forward, vertical first 向前自動滾動,垂直優先 - + Autoscroll backward, vertical first 向後自動滾動,垂直優先 - + Move down 向下移動 - + Move up 向上移動 - + Move left 向左移動 - + Move right 向右移動 - + Go to the first page 轉到第一頁 - + Go to the last page 轉到最後一頁 - + Offset double page to the left 雙頁向左偏移 - + Offset double page to the right 雙頁向右偏移 - - + + Reading 閱讀 - + There is a new version available 有新版本可用 - + Do you want to download the new version? 你要下載新版本嗎? - + Remind me in 14 days 14天後提醒我 - + Not now 現在不 @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + + Keyboard shortcuts + 鍵盤快捷鍵 + + + + Customize the keyboard shortcuts used by the application. + 自訂應用程式使用的鍵盤快捷鍵。 + + + Edit shortcuts 編輯快捷鍵 - + Shortcuts 快捷鍵 diff --git a/YACReader/yacreader_zh_TW.ts b/YACReader/yacreader_zh_TW.ts index 267c99bcf..4c7fb9b82 100644 --- a/YACReader/yacreader_zh_TW.ts +++ b/YACReader/yacreader_zh_TW.ts @@ -92,24 +92,24 @@ 無法載入目前主題 JSON。 - + Import theme 導入主題 - + JSON files (*.json);;All files (*) JSON 檔案 (*.json);;所有檔案 (*) - + Could not import theme from: %1 無法從以下位置匯入主題: %1 - + Could not import theme from: %1 @@ -120,7 +120,7 @@ %2 - + Import failed 導入失敗 @@ -152,7 +152,7 @@ ContinuousPageWidget - + Loading page %1 正在載入頁面 %1 @@ -188,25 +188,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + GoToDialog @@ -248,25 +253,30 @@ HelpAboutDialog - + About 關於 - + Help 幫助 - + System info 系統資訊 + + + Changelog + + OptionsDialog - + "Go to flow" size 「前往 Comic Flow」大小 @@ -276,57 +286,57 @@ 我的漫畫路徑 - + Background color 背景顏色 - + Choose 選擇 - + Quick Navigation Mode 快速導航模式 - + Disable mouse over activation 禁用滑鼠啟動 - + Scaling 縮放 - + Scaling method 縮放方法 - + Nearest (fast, low quality) 最近(快速,低品質) - + Bilinear 雙線性 - + Lanczos (better quality) Lanczos(品質更好) - + Restart is needed 需要重啟 - + Brightness 亮度 @@ -341,92 +351,152 @@ 在目前頁面資訊標籤中顯示時間 - + + Magnifying glass + 放大鏡 + + + + Circular magnifying glass + 圓形放大鏡 + + + + Draw a ring around the circular magnifying glass + 在圓形放大鏡周圍繪製邊框 + + + + Ease cursor movement toward the edges + 平滑游標移向邊緣的移動 + + + Scroll behaviour 滾動效果 - + Disable scroll animations and smooth scrolling 停用滾動動畫和平滑滾動 - + Do not turn page using scroll 滾動時不翻頁 - + Use single scroll step to turn page 使用單滾動步驟翻頁 - + Mouse mode 滑鼠模式 - + Only Back/Forward buttons can turn pages 只有後退/前進按鈕可以翻頁 - + Use the Left/Right buttons to turn pages. 使用向左/向右按鈕翻頁。 - + Click left or right half of the screen to turn pages. 點擊螢幕的左半部或右半部即可翻頁。 - + + Escape key + Esc 鍵 + + + + Quit the reader + 退出閱讀器 + + + + Cancel the active mode + 取消目前模式 + + + + Escape closes the reader, even while a mode is active. + 即使有模式正在使用,按 Esc 鍵仍會關閉閱讀器。 + + + + Escape cancels the first of these that is active: + +1. Magnifying glass +2. Dictionary +3. Go to page bar +4. Fullscreen + +If none is active, Escape does nothing. + Tooltip listing the order in which modes are cancelled. Only the first active mode in the list is cancelled per Escape keypress. + 按 Esc 鍵會取消下列第一個正在使用的模式: + +1. 放大鏡 +2. 字典 +3. 前往頁面列 +4. 全螢幕 + +若沒有任何模式正在使用,按 Esc 鍵不會執行任何操作。 + + + Contrast 對比度 - + Gamma Gamma值 - + Reset 重置 - + Image options 圖片選項 - + Fit options 適應項 - + Enlarge images to fit width/height 放大圖片以適應寬度/高度 - + Double Page options 雙頁選項 - + Show covers as single page 顯示封面為單頁 - + General 常規 - + Appearance 外貌 @@ -446,27 +516,27 @@ 系統預設 - + Clear 清空 - + Page Flow 頁面流 - + Image adjustment 圖像調整 - + Options 選項 - + Comics directory 漫畫目錄 @@ -704,48 +774,48 @@ Viewer - - + + Press 'O' to open comic. 按下 'O' 以打開漫畫. - + Not found 未找到 - + Comic not found 未找到漫畫 - + Error opening comic 打開漫畫時發生錯誤 - + CRC Error CRC 校驗失敗 - + Loading...please wait! 載入中... 請稍候! - + Page not available! 頁面不可用! - + Cover! 封面! - + Last page! 尾頁! @@ -871,545 +941,551 @@ YACReader::MainWindowViewer - + &Open 打開(&O) - + Open a comic 打開漫畫 - + New instance 新建實例 - + Open Folder 打開檔夾 - + Open image folder 打開圖片檔夾 - + Open latest comic 打開最近的漫畫 - + Open the latest comic opened in the previous reading session 打開最近閱讀漫畫 - + Clear 清空 - + Clear open recent list 清空最近訪問列表 - + Save 保存 - - + + Save current page 保存當前頁面 - - - - + + + + Extract page(s) - + Extract page(s) from the original source - + Previous Comic 上一個漫畫 - - - + + + Open previous comic 打開上一個漫畫 - + Next Comic 下一個漫畫 - - - + + + Open next comic 打開下一個漫畫 - + &Previous 上一頁(&P) - - - + + + Go to previous page 轉至上一頁 - + &Next 下一頁(&N) - - - + + + Go to next page 轉至下一頁 - + Fit Height 適應高度 - + Fit image to height 縮放圖片以適應高度 - + Fit Width 適合寬度 - + Fit image to width 縮放圖片以適應寬度 - + Show full size 顯示全尺寸 - + Fit to page 適應頁面 - + Continuous scroll 連續滾動 - + Switch to continuous scroll mode 切換到連續滾動模式 - + Reset zoom 重置縮放 - + Show zoom slider 顯示縮放滑塊 - + Zoom+ 放大 - + Zoom- 縮小 - + Rotate image to the left 向左旋轉圖片 - + Rotate image to the right 向右旋轉圖片 - + Double page mode 雙頁模式 - + Switch to double page mode 切換至雙頁模式 - + Double page manga mode 雙頁漫畫模式 - + Reverse reading order in double page mode 雙頁模式 (逆序閱讀) - + Go To 跳轉 - + Go to page ... 跳轉至頁面 ... - + Options 選項 - + YACReader options YACReader 選項 - - + + Help 幫助 - + Help, About YACReader 幫助, 關於 YACReader - + Magnifying glass 放大鏡 - + Switch Magnifying glass 切換放大鏡 - + Set bookmark 設置書簽 - + Set a bookmark on the current page 在當前頁面設置書簽 - + Show bookmarks 顯示書簽 - + Show the bookmarks of the current comic 顯示當前漫畫的書簽 - + Show keyboard shortcuts 顯示鍵盤快捷鍵 - + Show Info 顯示資訊 - + + Escape + Esc + + + + Escape key: quit, or cancel the active mode + Esc 鍵:退出或取消目前模式 + + + Close 關閉 - + Show Dictionary 顯示字典 - + Show go to flow 顯示「前往 Comic Flow」 - + Edit shortcuts 編輯快捷鍵 - + &File 檔(&F) - - + + Open recent 最近打開的檔 - + File - + Edit 編輯 - + View 查看 - + Go 轉到 - + Window 窗口 - - - + Open Comic 打開漫畫 - - - + Comic files 漫畫檔 - + Open folder 打開檔夾 - - + + Comics 漫畫 - + Toggle fullscreen mode 切換全屏模式 - + Hide/show toolbar 隱藏/顯示 工具欄 - - + + General 常規 - + Size up magnifying glass 增大放大鏡尺寸 - + Size down magnifying glass 減小放大鏡尺寸 - + Zoom in magnifying glass 增大縮放級別 - + Zoom out magnifying glass 減小縮放級別 - + Reset magnifying glass 重置放大鏡 - - + + Magnifiying glass 放大鏡 - + Toggle between fit to width and fit to height 切換顯示為"適應寬度"或"適應高度" - - + + Page adjustement 頁面調整 - + Overwrite file? - + The file already exists. Do you want to overwrite it? - + The current page could not be extracted. - + Overwrite files? - + Some files already exist. Do you want to overwrite them? - + Some pages could not be extracted. - + Autoscroll down 向下自動滾動 - + Autoscroll up 向上自動滾動 - + Autoscroll forward, horizontal first 向前自動滾動,水準優先 - + Autoscroll backward, horizontal first 向後自動滾動,水準優先 - + Autoscroll forward, vertical first 向前自動滾動,垂直優先 - + Autoscroll backward, vertical first 向後自動滾動,垂直優先 - + Move down 向下移動 - + Move up 向上移動 - + Move left 向左移動 - + Move right 向右移動 - + Go to the first page 轉到第一頁 - + Go to the last page 轉到最後一頁 - + Offset double page to the left 雙頁向左偏移 - + Offset double page to the right 雙頁向右偏移 - - + + Reading 閱讀 - + There is a new version available 有新版本可用 - + Do you want to download the new version? 你要下載新版本嗎? - + Remind me in 14 days 14天後提醒我 - + Not now 現在不 @@ -1417,12 +1493,12 @@ YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -1460,22 +1536,32 @@ YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + + Keyboard shortcuts + 鍵盤快速鍵 + + + + Customize the keyboard shortcuts used by the application. + 自訂應用程式使用的鍵盤快速鍵。 + + + Edit shortcuts 編輯快捷鍵 - + Shortcuts 快捷鍵 diff --git a/YACReaderLibrary/CMakeLists.txt b/YACReaderLibrary/CMakeLists.txt index ffb2e7217..803149a7c 100644 --- a/YACReaderLibrary/CMakeLists.txt +++ b/YACReaderLibrary/CMakeLists.txt @@ -10,6 +10,8 @@ add_library(library_common STATIC bundle_creator.cpp initial_comic_info_extractor.h initial_comic_info_extractor.cpp + comic_info_repairer.h + comic_info_repairer.cpp xml_info_parser.h xml_info_parser.cpp xml_info_library_scanner.h @@ -54,6 +56,8 @@ add_library(db_helper STATIC db/reading_list.cpp db/query_lexer.h db/query_lexer.cpp + db/search_field_registry.h + db/search_field_registry.cpp db/query_parser.h db/query_parser.cpp db/search_query.h @@ -92,6 +96,8 @@ qt_add_executable(YACReaderLibrary WIN32 properties_dialog.cpp options_dialog.h options_dialog.cpp + search_syntax_dialog.h + search_syntax_dialog.cpp export_library_dialog.h export_library_dialog.cpp import_library_dialog.h @@ -204,6 +210,7 @@ target_compile_definitions(YACReaderLibrary PRIVATE # Resources set(yacreaderlibrary_image_files + ${PROJECT_SOURCE_DIR}/images/chevronDown.svg ${PROJECT_SOURCE_DIR}/images/shortcuts/accept_shortcut.svg ${PROJECT_SOURCE_DIR}/images/shortcuts/clear_shortcut.svg ${PROJECT_SOURCE_DIR}/images/comic_vine/downArrow.svg diff --git a/YACReaderLibrary/comic_info_repairer.cpp b/YACReaderLibrary/comic_info_repairer.cpp new file mode 100644 index 000000000..8a8052ba5 --- /dev/null +++ b/YACReaderLibrary/comic_info_repairer.cpp @@ -0,0 +1,601 @@ +#include "comic_info_repairer.h" + +#include "QsLog.h" +#include "comic_db.h" +#include "cover_utils.h" +#include "data_base_management.h" +#include "db_helper.h" +#include "initial_comic_info_extractor.h" +#include "xml_info_parser.h" +#include "yacreader_global.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace YACReader; + +namespace { + +// deterministic suffixes so leftovers from an interrupted repair can be swept on the next run +// the temporary cover keeps the .jpg suffix so QImage::save can infer the format +constexpr auto repairNewCoverSuffix = ".repair-new.jpg"; +constexpr auto repairOldCoverSuffix = ".repair-old"; + +enum class RepairCase { + InvalidPageCount, + MissingCover, + MissingCoverRatio, + Healthy +}; + +// Everything a repair needs to know about a broken comic; the full comic_info +// row is only loaded lazily, one comic at a time, when a repair requires it. +struct RepairTask { + RepairCase repairCase { RepairCase::Healthy }; + qulonglong id { 0 }; + QString hash; + int coverPage { 1 }; + bool usesExternalCover { false }; + QStringList relativePaths; +}; + +struct ExtractedInfo { + int numPages { 0 }; + QImage cover; + QPair originalCoverSize { 0, 0 }; + QByteArray xmlData; + bool hasValidCover { false }; +}; + +bool hasValidRatio(const QVariant &coverSizeRatio) +{ + bool converted = false; + const auto ratio = coverSizeRatio.toDouble(&converted); + return converted && std::isfinite(ratio) && ratio > 0; +} + +RepairCase classify(const QVariant &numPages, const QVariant &coverSizeRatio, bool coverExists) +{ + if (numPages.isNull() || numPages.toInt() <= 0) { + return RepairCase::InvalidPageCount; + } + + if (!coverExists) { + return RepairCase::MissingCover; + } + + if (!hasValidRatio(coverSizeRatio)) { + return RepairCase::MissingCoverRatio; + } + + return RepairCase::Healthy; +} + +// keeps the peak memory usage and the lifetime of the scan cursor bounded no +// matter how many comics need repair +constexpr int scanBatchSize = 1000; + +struct ScanResult { + QList tasks; + qulonglong lastScannedId { 0 }; + bool scanComplete { true }; +}; + +// Streams over the comics with id > afterId classifying them on the fly and +// keeps only the broken ones, stopping after scanBatchSize tasks. The scan +// only stops at group boundaries, so a comic available at several paths is +// never split across batches. +ScanResult findRepairTasks(QSqlDatabase &db, const QString &target, qulonglong afterId, const std::atomic_bool &stopRequested, QString &error) +{ + ScanResult result; + result.lastScannedId = afterId; + + QSqlQuery query(db); + // without this the SQLite driver caches every visited row to support + // backward navigation, defeating the batched scan's memory bound + query.setForwardOnly(true); + query.prepare("SELECT ci.id, ci.hash, ci.numPages, ci.coverSizeRatio, ci.coverPage, ci.usesExternalCover, c.path " + "FROM comic_info ci " + "INNER JOIN comic c ON c.comicInfoId = ci.id " + "WHERE ci.id > :afterId " + "ORDER BY ci.id, c.id"); + query.bindValue(":afterId", afterId); + + if (!query.exec()) { + error = query.lastError().text(); + QLOG_ERROR() << "Unable to load comics for repair:" << error; + return result; + } + + qulonglong lastSeenId = afterId; + bool lastNeedsRepair = false; + while (query.next()) { + if (stopRequested.load(std::memory_order_acquire)) { + result.scanComplete = false; + break; + } + + const auto id = query.value(0).toULongLong(); + const auto relativePath = query.value(6).toString(); + + // rows are grouped by comic_info id, extra rows of a group are extra paths of the same comic + if (id == lastSeenId) { + if (lastNeedsRepair) { + result.tasks.last().relativePaths.append(relativePath); + } + continue; + } + + if (result.tasks.size() >= scanBatchSize) { + // batch full, the next batch will resume from the last fully scanned comic + result.scanComplete = false; + break; + } + lastSeenId = id; + + const auto hash = query.value(1).toString(); + const auto coverExists = QFile::exists(LibraryPaths::coverPathFromLibraryDataPath(target, hash)); + const auto repairCase = classify(query.value(2), query.value(3), coverExists); + + lastNeedsRepair = repairCase != RepairCase::Healthy; + if (lastNeedsRepair) { + RepairTask task; + task.repairCase = repairCase; + task.id = id; + task.hash = hash; + task.coverPage = query.value(4).toInt(); + task.usesExternalCover = query.value(5).toBool(); + task.relativePaths.append(relativePath); + result.tasks.append(task); + } + } + + result.lastScannedId = lastSeenId; + return result; +} + +QString existingComicPath(const QString &source, const QStringList &relativePaths, QString &relativePath) +{ + for (const auto &path : relativePaths) { + const auto absolutePath = QDir::cleanPath(source + path); + const QFileInfo fileInfo(absolutePath); + if (fileInfo.exists() && fileInfo.isFile()) { + relativePath = path; + return absolutePath; + } + } + + relativePath = relativePaths.value(0); + return { }; +} + +ExtractedInfo extractInfo(const QString &comicPath, int coverPage, bool importXmlMetadata) +{ + if (coverPage <= 0) { + coverPage = 1; + } + + InitialComicInfoExtractor extractor(comicPath, QString(), coverPage, importXmlMetadata); + extractor.extract(); + + ExtractedInfo result; + result.numPages = extractor.getNumPages(); + result.cover = extractor.getCoverImage(); + result.originalCoverSize = extractor.getOriginalCoverSize(); + result.xmlData = extractor.getXMLInfoRawData(); + result.hasValidCover = extractor.hasValidCover(); + + if (result.numPages > 0 && !result.hasValidCover && coverPage != 1) { + InitialComicInfoExtractor fallbackExtractor(comicPath, QString(), 1, importXmlMetadata); + fallbackExtractor.extract(); + + result.numPages = fallbackExtractor.getNumPages(); + result.cover = fallbackExtractor.getCoverImage(); + result.originalCoverSize = fallbackExtractor.getOriginalCoverSize(); + if (result.xmlData.isEmpty()) { + result.xmlData = fallbackExtractor.getXMLInfoRawData(); + } + result.hasValidCover = fallbackExtractor.hasValidCover(); + } + + return result; +} + +QString coverSizeString(const QPair &size) +{ + return QString("%1x%2").arg(size.first).arg(size.second); +} + +double coverRatio(const QPair &size) +{ + return static_cast(size.first) / static_cast(size.second); +} + +void bindXmlMetadata(QSqlQuery &query, const ComicInfo &info) +{ + for (const auto &field : xmlMetadataFields()) { + const auto placeholder = QString(":") + field.column; + if (qstrcmp(field.column, "type") == 0) { + // type holds a FileType enum, store it as int like DBHelper::update does + query.bindValue(placeholder, static_cast(info.type.value())); + } else { + query.bindValue(placeholder, info.*(field.member)); + } + } +} + +void prepareInvalidPageCountUpdate(QSqlQuery &query, + const ComicInfo &info, + const ExtractedInfo &extracted, + bool preserveCustomCover, + const QImage &customCover, + bool xmlParsed, + const ComicInfo &updatedInfo) +{ + QStringList assignments { "numPages = :numPages" }; + + if (preserveCustomCover) { + if (!hasValidRatio(info.coverSizeRatio)) { + assignments.append("coverSizeRatio = :coverSizeRatio"); + } + } else { + assignments.append("originalCoverSize = :originalCoverSize"); + assignments.append("coverSizeRatio = :coverSizeRatio"); + if (info.usesExternalCover.toBool()) { + assignments.append("usesExternalCover = 0"); + assignments.append("lastTimeCoverSet = 0"); + } + } + + if (xmlParsed) { + for (const auto &field : xmlMetadataFields()) { + assignments.append(QString("%1 = :%1").arg(field.column)); + } + } + + query.prepare(QString("UPDATE comic_info SET %1 WHERE id = :id").arg(assignments.join(", "))); + query.bindValue(":numPages", extracted.numPages); + query.bindValue(":id", info.id); + + if (preserveCustomCover) { + if (!hasValidRatio(info.coverSizeRatio)) { + query.bindValue(":coverSizeRatio", static_cast(customCover.width()) / customCover.height()); + } + } else { + query.bindValue(":originalCoverSize", coverSizeString(extracted.originalCoverSize)); + query.bindValue(":coverSizeRatio", coverRatio(extracted.originalCoverSize)); + } + + if (xmlParsed) { + bindXmlMetadata(query, updatedInfo); + } +} + +void prepareMissingCoverUpdate(QSqlQuery &query, const RepairTask &task, const ExtractedInfo &extracted) +{ + QStringList assignments { + "originalCoverSize = :originalCoverSize", + "coverSizeRatio = :coverSizeRatio", + }; + if (task.usesExternalCover) { + assignments.append("usesExternalCover = 0"); + assignments.append("lastTimeCoverSet = 0"); + } + + query.prepare(QString("UPDATE comic_info SET %1 WHERE id = :id").arg(assignments.join(", "))); + query.bindValue(":originalCoverSize", coverSizeString(extracted.originalCoverSize)); + query.bindValue(":coverSizeRatio", coverRatio(extracted.originalCoverSize)); + query.bindValue(":id", task.id); +} + +bool executeUpdate(QSqlDatabase &db, QSqlQuery &query) +{ + if (!db.transaction()) { + QLOG_ERROR() << "Unable to start comic info repair transaction:" << db.lastError().text(); + return false; + } + + if (!query.exec() || query.numRowsAffected() != 1) { + QLOG_ERROR() << "Unable to update repaired comic info:" << query.lastError().text(); + db.rollback(); + return false; + } + + if (!db.commit()) { + QLOG_ERROR() << "Unable to commit repaired comic info:" << db.lastError().text(); + db.rollback(); + return false; + } + + return true; +} + +bool restorePreviousCover(const QString &coverPath, const QString &backupPath) +{ + if (QFile::exists(coverPath) && !QFile::remove(coverPath)) { + QLOG_ERROR() << "Unable to remove failed repaired cover:" << coverPath; + return false; + } + + if (!backupPath.isEmpty() && !QFile::rename(backupPath, coverPath)) { + QLOG_ERROR() << "Unable to restore previous cover:" << backupPath; + return false; + } + + return true; +} + +bool executeUpdateWithCover(QSqlDatabase &db, QSqlQuery &query, const QString &coverPath, const QImage &cover) +{ + const auto temporaryPath = coverPath + repairNewCoverSuffix; + const auto backupPath = QFile::exists(coverPath) ? coverPath + repairOldCoverSuffix : QString(); + + if (!QDir().mkpath(QFileInfo(coverPath).absolutePath()) || !saveCover(temporaryPath, cover)) { + QFile::remove(temporaryPath); + QLOG_ERROR() << "Unable to save temporary repaired cover:" << temporaryPath; + return false; + } + + if (!db.transaction()) { + QFile::remove(temporaryPath); + QLOG_ERROR() << "Unable to start comic info repair transaction:" << db.lastError().text(); + return false; + } + + if (!query.exec() || query.numRowsAffected() != 1) { + QFile::remove(temporaryPath); + db.rollback(); + QLOG_ERROR() << "Unable to update repaired comic info:" << query.lastError().text(); + return false; + } + + if (!backupPath.isEmpty() && !QFile::rename(coverPath, backupPath)) { + QFile::remove(temporaryPath); + db.rollback(); + QLOG_ERROR() << "Unable to back up existing cover:" << coverPath; + return false; + } + + if (!QFile::rename(temporaryPath, coverPath)) { + if (!backupPath.isEmpty()) { + QFile::rename(backupPath, coverPath); + } + db.rollback(); + QLOG_ERROR() << "Unable to install repaired cover:" << coverPath; + return false; + } + + if (!db.commit()) { + restorePreviousCover(coverPath, backupPath); + db.rollback(); + QLOG_ERROR() << "Unable to commit repaired comic info:" << db.lastError().text(); + return false; + } + + if (!backupPath.isEmpty()) { + QFile::remove(backupPath); + } + return true; +} + +} + +ComicInfoRepairer::ComicInfoRepairer(QSettings *settings, QObject *parent) + : QThread(parent), settings(settings) +{ +} + +void ComicInfoRepairer::repairLibrary(const QString &source, const QString &target, bool removeStaleLock) +{ + if (isRunning()) { + return; + } + + this->source = QDir::cleanPath(source); + this->target = QDir::cleanPath(target); + this->removeStaleLock = removeStaleLock; + importXmlMetadata = settings && settings->value(IMPORT_COMIC_INFO_XML_METADATA, false).toBool(); + stopRequested.store(false, std::memory_order_release); + repairSummary = { }; + start(); +} + +ComicInfoRepairSummary ComicInfoRepairer::summary() const +{ + return repairSummary; +} + +void ComicInfoRepairer::stop() +{ + stopRequested.store(true, std::memory_order_release); +} + +void ComicInfoRepairer::run() +{ +#if !defined use_unarr && !defined use_libarchive + std::unique_ptr sevenzLibrary; + bool sevenzLibraryLoaded = false; + auto ensure7zLibraryLoaded = [&]() { + if (!sevenzLibrary) { + sevenzLibrary.reset(load7zLibrary()); + sevenzLibrary->setLoadHints(QLibrary::PreventUnloadHint); + sevenzLibraryLoaded = sevenzLibrary->load(); + } + return sevenzLibraryLoaded; + }; +#endif + + LibraryMaintenanceLock maintenanceLock(source); + if (!maintenanceLock.tryLock(removeStaleLock)) { + repairSummary.lockedByAnotherProcess = true; + repairSummary.lockHolderInfo = maintenanceLock.holderInfo(); + repairSummary.lockHolderIsRunningLocally = maintenanceLock.holderIsRunningLocally(); + repairSummary.error = maintenanceLock.errorString(); + return; + } + + QString backupError; + if (!DataBaseManagement::backupLibrary(source, DatabaseBackupReason::BeforeRepair, &backupError)) { + repairSummary.error = QString("Unable to back up library database: %1").arg(backupError); + QLOG_ERROR() << repairSummary.error; + emit failed(repairSummary.error); + return; + } + + QString connectionName; + { + auto db = DataBaseManagement::loadDatabase(target); + if (!db.isValid() || !db.isOpen()) { + repairSummary.error = QString("Unable to open database at: %1").arg(target); + emit failed(repairSummary.error); + return; + } + connectionName = db.connectionName(); + + // sweep leftovers from a previously interrupted repair + { + const auto coversPath = LibraryPaths::libraryCoversPathFromLibraryDataPath(target); + + // a repair interrupted between backing up the old cover and installing + // the new one leaves the only copy of the cover in the backup: restore + // it instead of deleting it + QDirIterator backups(coversPath, { QString("*") + repairOldCoverSuffix }, QDir::Files); + while (backups.hasNext()) { + const auto backupPath = backups.next(); + const auto coverPath = backupPath.chopped(qstrlen(repairOldCoverSuffix)); + if (QFile::exists(coverPath)) { + QFile::remove(backupPath); + } else { + QFile::rename(backupPath, coverPath); + } + } + + QDirIterator leftovers(coversPath, { QString("*") + repairNewCoverSuffix }, QDir::Files); + while (leftovers.hasNext()) { + QFile::remove(leftovers.next()); + } + } + + qulonglong afterId = 0; + bool moreToScan = true; + while (moreToScan) { + if (stopRequested.load(std::memory_order_acquire)) { + repairSummary.canceled = true; + break; + } + + QString scanError; + const auto scan = findRepairTasks(db, target, afterId, stopRequested, scanError); + if (!scanError.isEmpty()) { + repairSummary.error = scanError; + emit failed(repairSummary.error); + break; + } + moreToScan = !scan.scanComplete; + afterId = scan.lastScannedId; + + for (const auto &task : scan.tasks) { + if (stopRequested.load(std::memory_order_acquire)) { + repairSummary.canceled = true; + break; + } + + const auto repairCase = task.repairCase; + const auto coverPath = LibraryPaths::coverPathFromLibraryDataPath(target, task.hash); + auto relativePath = task.relativePaths.value(0); + + const auto comicPath = existingComicPath(source, task.relativePaths, relativePath); + if (comicPath.isEmpty()) { + repairSummary.missingFiles++; + repairSummary.missingFilePaths.append(relativePath); + emit comicProcessed(relativePath, coverPath); + continue; + } + +#if !defined use_unarr && !defined use_libarchive + if (repairCase == RepairCase::InvalidPageCount || repairCase == RepairCase::MissingCover) { +#ifndef NO_PDF + const bool needs7zLibrary = QFileInfo(comicPath).suffix().compare("pdf", Qt::CaseInsensitive) != 0; +#else + const bool needs7zLibrary = true; +#endif + if (needs7zLibrary && !ensure7zLibraryLoaded()) { + repairSummary.error = sevenzLibrary->errorString(); + emit failed(repairSummary.error); + break; + } + } +#endif + + bool repaired = false; + if (repairCase == RepairCase::InvalidPageCount) { + // this repair needs the full comic_info row: it is the base the parsed XML metadata is merged over + const auto info = DBHelper::loadComicInfo(task.hash, db); + QImage customCover; + const bool preserveCustomCover = info.existOnDb && info.usesExternalCover.toBool() && customCover.load(coverPath); + const auto extracted = extractInfo(comicPath, task.coverPage, importXmlMetadata); + + if (info.existOnDb && extracted.numPages > 0 && (preserveCustomCover || (extracted.hasValidCover && extracted.originalCoverSize.second > 0))) { + auto updatedInfo = info; + const bool xmlParsed = importXmlMetadata && parseXMLIntoInfo(extracted.xmlData, updatedInfo); + QSqlQuery update(db); + prepareInvalidPageCountUpdate(update, info, extracted, preserveCustomCover, customCover, xmlParsed, updatedInfo); + if (preserveCustomCover) { + repaired = executeUpdate(db, update); + } else { + repaired = executeUpdateWithCover(db, update, coverPath, extracted.cover); + } + } + } else if (repairCase == RepairCase::MissingCover) { + const auto extracted = extractInfo(comicPath, task.coverPage, false); + if (extracted.hasValidCover && extracted.originalCoverSize.second > 0) { + QSqlQuery update(db); + prepareMissingCoverUpdate(update, task, extracted); + repaired = executeUpdateWithCover(db, update, coverPath, extracted.cover); + } + } else if (repairCase == RepairCase::MissingCoverRatio) { + QImage cover; + if (cover.load(coverPath) && cover.height() > 0) { + QSqlQuery update(db); + update.prepare("UPDATE comic_info SET coverSizeRatio = :coverSizeRatio WHERE id = :id"); + update.bindValue(":coverSizeRatio", static_cast(cover.width()) / cover.height()); + update.bindValue(":id", task.id); + repaired = executeUpdate(db, update); + } + } + + if (repaired) { + repairSummary.repaired++; + } else { + repairSummary.failed++; + repairSummary.failedFilePaths.append(relativePath); + } + emit comicProcessed(relativePath, coverPath); + } + + // propagate cancellation or a fatal error out of the batch loop + if (repairSummary.canceled || !repairSummary.error.isEmpty()) { + break; + } + } + + db.close(); + } + + if (!connectionName.isEmpty()) { + QSqlDatabase::removeDatabase(connectionName); + } +} diff --git a/YACReaderLibrary/comic_info_repairer.h b/YACReaderLibrary/comic_info_repairer.h new file mode 100644 index 000000000..a11a574ae --- /dev/null +++ b/YACReaderLibrary/comic_info_repairer.h @@ -0,0 +1,63 @@ +#ifndef COMIC_INFO_REPAIRER_H +#define COMIC_INFO_REPAIRER_H + +#include +#include + +#include + +class QSettings; + +namespace YACReader { + +struct ComicInfoRepairSummary { + int repaired { 0 }; + int failed { 0 }; + int missingFiles { 0 }; + bool canceled { false }; + QString error; + QStringList failedFilePaths; + QStringList missingFilePaths; + // set when another repair holds the library lock; holder info comes from the + // lock file and is empty if it couldn't be read + bool lockedByAnotherProcess { false }; + bool lockHolderIsRunningLocally { false }; + QString lockFilePath; + QString lockHolderInfo; +}; + +class ComicInfoRepairer : public QThread +{ + Q_OBJECT + +public: + explicit ComicInfoRepairer(QSettings *settings, QObject *parent = nullptr); + + // removeStaleLock removes an existing lock file before locking, for retrying + // after the user confirmed that the previous holder is not running anymore + void repairLibrary(const QString &source, const QString &target, bool removeStaleLock = false); + ComicInfoRepairSummary summary() const; + +public slots: + void stop(); + +signals: + void comicProcessed(const QString &relativePath, const QString &coverPath); + void failed(const QString &error); + +protected: + void run() override; + +private: + QSettings *settings; + QString source; + QString target; + bool importXmlMetadata { false }; + bool removeStaleLock { false }; + std::atomic_bool stopRequested { false }; + ComicInfoRepairSummary repairSummary; +}; + +} + +#endif // COMIC_INFO_REPAIRER_H diff --git a/YACReaderLibrary/db/comic_model.cpp b/YACReaderLibrary/db/comic_model.cpp index 501d09520..0368175f1 100644 --- a/YACReaderLibrary/db/comic_model.cpp +++ b/YACReaderLibrary/db/comic_model.cpp @@ -1,6 +1,7 @@ #include "comic_model.h" +#include "comic.h" #include "comic_db.h" #include "comic_item.h" #include "data_base_management.h" @@ -22,15 +23,8 @@ #include "QsLog.h" auto defaultFolderContentSortFunction = [](const ComicItem *c1, const ComicItem *c2) { - if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) { - return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); - } else { - if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { - return naturalSortLessThanCI(c1->data(ComicModel::Number).toString(), c2->data(ComicModel::Number).toString()); - } else { - return c2->data(ComicModel::Number).isNull(); - } - } + return comicNumberLessThan(c1->data(ComicModel::Number), c1->data(ComicModel::FileName).toString(), + c2->data(ComicModel::Number), c2->data(ComicModel::FileName).toString()); }; ComicModel::ComicModel(QObject *parent) @@ -451,6 +445,8 @@ QVariant ComicModel::headerData(int section, Qt::Orientation orientation, return QVariant(QIcon(":/images/comicRar.png")); else if (ext.compare("cbz", Qt::CaseInsensitive) == 0) return QVariant(QIcon(":/images/comicZip.png")); + else if (Comic::fileIsEpub(fileName)) + return QVariant(QIcon(":/images/comicZip.png")); else if (ext.compare("pdf", Qt::CaseInsensitive) == 0) return QVariant(QIcon(":/images/pdf.png")); else if (ext.compare("tar", Qt::CaseInsensitive) == 0) diff --git a/YACReaderLibrary/db/comic_query_result_processor.cpp b/YACReaderLibrary/db/comic_query_result_processor.cpp index edfe35d5a..6d473c358 100644 --- a/YACReaderLibrary/db/comic_query_result_processor.cpp +++ b/YACReaderLibrary/db/comic_query_result_processor.cpp @@ -54,15 +54,8 @@ QList *YACReader::ComicQueryResultProcessor::modelData(QSqlQuery &s } std::sort(list->begin(), list->end(), [](const ComicItem *c1, const ComicItem *c2) { - if (c1->data(ComicModel::Number).isNull() && c2->data(ComicModel::Number).isNull()) { - return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); - } else { - if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { - return c1->data(ComicModel::Number).toInt() < c2->data(ComicModel::Number).toInt(); - } else { - return c2->data(ComicModel::Number).isNull(); - } - } + return comicNumberLessThan(c1->data(ComicModel::Number), c1->data(ComicModel::FileName).toString(), + c2->data(ComicModel::Number), c2->data(ComicModel::FileName).toString()); }); return list; diff --git a/YACReaderLibrary/db/data_base_management.cpp b/YACReaderLibrary/db/data_base_management.cpp index 5a2389be0..fe3f3bedc 100644 --- a/YACReaderLibrary/db/data_base_management.cpp +++ b/YACReaderLibrary/db/data_base_management.cpp @@ -10,8 +10,319 @@ #include #include +#ifdef Q_OS_WIN +#include +#else +#include +#endif + using namespace YACReader; +namespace { +const auto backupTimestampFormat = QStringLiteral("yyyyMMdd-HHmmss"); + +QString backupReasonName(DatabaseBackupReason reason) +{ + switch (reason) { + case DatabaseBackupReason::AutoUpdate: + return "auto-update"; + case DatabaseBackupReason::BeforeUpgrade: + return "before-upgrade"; + case DatabaseBackupReason::BeforeRepair: + return "before-repair"; + case DatabaseBackupReason::BeforeRestore: + return "before-restore"; + case DatabaseBackupReason::Manual: + return "manual"; + } + return { }; +} + +bool validateDatabase(const QString &path, QString *error, QString *versionOut = nullptr) +{ + QString connectionName; + bool valid = false; + { + auto db = DataBaseManagement::loadDatabaseFromFile(path); + connectionName = db.connectionName(); + if (!db.isOpen()) { + if (error) + *error = QString("Unable to open database: %1").arg(path); + } else { + QSqlQuery versionQuery(db); + QSqlQuery check(db); + valid = versionQuery.exec("SELECT version FROM db_info") && versionQuery.next() && check.exec("PRAGMA quick_check") && check.next() && check.value(0).toString() == "ok"; + if (valid && versionOut) + *versionOut = versionQuery.value(0).toString(); + if (!valid && error) + *error = QString("Database validation failed: %1").arg(path); + } + } + if (!connectionName.isEmpty()) + QSqlDatabase::removeDatabase(connectionName); + return valid; +} + +bool vacuumDatabaseInto(const QString &sourcePath, const QString &destinationPath, QString *error) +{ + QString connectionName; + bool created = false; + { + auto db = DataBaseManagement::loadDatabaseFromFile(sourcePath); + connectionName = db.connectionName(); + if (!db.isOpen()) { + if (error) + *error = QString("Unable to open database: %1").arg(sourcePath); + } else { + QString escapedPath = QDir::toNativeSeparators(destinationPath); + escapedPath.replace('\'', "''"); + QSqlQuery vacuum(db); + created = vacuum.exec(QString("VACUUM main INTO '%1'").arg(escapedPath)); + if (!created && error) + *error = vacuum.lastError().text(); + } + } + if (!connectionName.isEmpty()) + QSqlDatabase::removeDatabase(connectionName); + return created; +} + +QFileInfoList backupsForReason(const QDir &directory, const QString &reason) +{ + return directory.entryInfoList({ QString("library-*-*-%1.ydb").arg(reason) }, QDir::Files, QDir::Name | QDir::Reversed); +} + +QDateTime backupDateTime(const QFileInfo &backup) +{ + constexpr qsizetype prefixLength = 8; // "library-" + constexpr qsizetype timestampLength = 15; + return QDateTime::fromString(backup.completeBaseName().mid(prefixLength, timestampLength), backupTimestampFormat); +} + +void removeOldBackups(const QDir &directory, DatabaseBackupReason reason, const QString &newBackup, const QString &protectedBackup = { }) +{ + const auto name = backupReasonName(reason); + const auto backups = backupsForReason(directory, name); + QSet keep; + keep.insert(QFileInfo(newBackup).absoluteFilePath()); + if (!protectedBackup.isEmpty()) + keep.insert(QFileInfo(protectedBackup).absoluteFilePath()); + + if (reason == DatabaseBackupReason::AutoUpdate) { + const auto now = QDateTime::currentDateTime(); + QSet olderMonths; + for (const auto &backup : backups) { + const auto created = backupDateTime(backup); + if (!created.isValid()) + continue; + const auto age = created.daysTo(now); + if (age < 14) { + keep.insert(backup.absoluteFilePath()); + } else if (age < 14 + 366) { + const auto month = created.toString("yyyy-MM"); + if (!olderMonths.contains(month)) { + olderMonths.insert(month); + keep.insert(backup.absoluteFilePath()); + } + } + } + } else { + int limit = reason == DatabaseBackupReason::Manual ? 10 : 3; + for (int i = 0; i < qMin(limit, backups.size()); ++i) + keep.insert(backups.at(i).absoluteFilePath()); + } + + for (const auto &backup : backups) { + if (!keep.contains(backup.absoluteFilePath())) { + QLOG_INFO() << "Removing old database backup" << backup.absoluteFilePath(); + QFile::remove(backup.absoluteFilePath()); + } + } +} + +const QStringList sqliteSidecarSuffixes { "-journal", "-wal", "-shm" }; + +QString restoreMarkerPath(const QString &databasePath) +{ + return databasePath + ".restore"; +} + +QString stagedDatabasePath(const QString &databasePath) +{ + return databasePath + ".staged"; +} + +QString rollbackDatabasePath(const QString &databasePath) +{ + return databasePath + ".rollback"; +} + +QString salvageDatabasePath(const QString &databasePath) +{ + return databasePath + ".salvage"; +} + +QString damagedDatabasePath(const QString &libraryPath) +{ + const auto recoveryDirectory = QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("recovery"); + return QDir(recoveryDirectory).filePath(QString("library-%1-damaged.ydb").arg(QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss-zzz"))); +} + +bool removeDatabaseUnit(const QString &databasePath) +{ + bool success = !QFile::exists(databasePath) || QFile::remove(databasePath); + for (const auto &suffix : sqliteSidecarSuffixes) { + const auto path = databasePath + suffix; + success = (!QFile::exists(path) || QFile::remove(path)) && success; + } + return success; +} + +bool moveDatabaseUnit(const QString &source, const QString &destination, QString *error) +{ + QStringList movedSuffixes; + const QStringList suffixes = { QString(), "-journal", "-wal", "-shm" }; + for (const auto &suffix : suffixes) { + const auto sourcePath = source + suffix; + if (!QFile::exists(sourcePath)) + continue; + + const auto destinationPath = destination + suffix; + if (QFile::exists(destinationPath) || !QFile::rename(sourcePath, destinationPath)) { + for (auto it = movedSuffixes.crbegin(); it != movedSuffixes.crend(); ++it) + QFile::rename(destination + *it, source + *it); + if (error) + *error = QString("Unable to move %1 to %2").arg(sourcePath, destinationPath); + return false; + } + movedSuffixes.append(suffix); + } + return true; +} + +bool copyDatabaseUnit(const QString &source, const QString &destination, QString *error) +{ + if (!QDir().mkpath(QFileInfo(destination).absolutePath())) { + if (error) + *error = QString("Unable to create recovery folder: %1").arg(QFileInfo(destination).absolutePath()); + return false; + } + + const QStringList suffixes = { QString(), "-journal", "-wal", "-shm" }; + for (const auto &suffix : suffixes) { + const auto sourcePath = source + suffix; + if (!QFile::exists(sourcePath)) + continue; + if (!QFile::copy(sourcePath, destination + suffix)) { + removeDatabaseUnit(destination); + if (error) + *error = QString("Unable to preserve damaged database: %1").arg(sourcePath); + return false; + } + } + return true; +} + +bool writeRestoreMarker(const QString &path, bool hadOriginal, const QString &stage, QString *error) +{ + QSaveFile marker(path); + if (!marker.open(QIODevice::WriteOnly)) { + if (error) + *error = marker.errorString(); + return false; + } + marker.write(hadOriginal ? "original=1\n" : "original=0\n"); + marker.write("stage=" + stage.toUtf8() + "\n"); + if (!marker.commit()) { + if (error) + *error = marker.errorString(); + return false; + } + return true; +} + +bool markerHadOriginal(const QString &path) +{ + QFile marker(path); + return marker.open(QIODevice::ReadOnly) && marker.readAll().contains("original=1\n"); +} + +bool replaceFileAtomically(const QString &source, const QString &destination, QString *error) +{ +#ifdef Q_OS_WIN + const bool replaced = MoveFileExW(reinterpret_cast(source.utf16()), reinterpret_cast(destination.utf16()), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH); +#else + const auto sourceName = QFile::encodeName(source); + const auto destinationName = QFile::encodeName(destination); + const bool replaced = std::rename(sourceName.constData(), destinationName.constData()) == 0; +#endif + if (!replaced && error) + *error = QString("Unable to replace destination file: %1").arg(destination); + return replaced; +} +} + +LibraryMaintenanceLock::LibraryMaintenanceLock(const QString &libraryPath) + : maintenanceLock(std::make_unique(QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("maintenance.lock"))), legacyRepairLock(std::make_unique(QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("repair.lock"))) +{ + maintenanceLock->setStaleLockTime(0); + legacyRepairLock->setStaleLockTime(0); +} + +LibraryMaintenanceLock::~LibraryMaintenanceLock() = default; + +bool LibraryMaintenanceLock::tryLockFile(QLockFile &lock, bool removeStaleLock) +{ + if (lock.tryLock()) + return true; + if (removeStaleLock && lock.removeStaleLockFile() && lock.tryLock()) + return true; + captureLockInfo(lock); + failedLockPath = lock.fileName(); + return false; +} + +bool LibraryMaintenanceLock::tryLock(bool removeStaleLock) +{ + failedLockPath.clear(); + currentHolderInfo.clear(); + currentHolderIsRunningLocally = false; + if (!tryLockFile(*maintenanceLock, removeStaleLock)) + return false; + if (!tryLockFile(*legacyRepairLock, removeStaleLock)) { + maintenanceLock->unlock(); + return false; + } + return true; +} + +void LibraryMaintenanceLock::captureLockInfo(QLockFile &lock) +{ + qint64 pid = 0; + QString hostname; + QString appname; + if (lock.getLockInfo(&pid, &hostname, &appname)) { + currentHolderInfo = QString("%1 (PID %2) on %3").arg(appname).arg(pid).arg(hostname.isEmpty() ? QString("unknown host") : hostname); + currentHolderIsRunningLocally = !hostname.isEmpty() && hostname == QSysInfo::machineHostName(); + } +} + +QString LibraryMaintenanceLock::errorString() const +{ + return QString("Another maintenance operation is using this library (%1)%2") + .arg(failedLockPath, currentHolderInfo.isEmpty() ? QString() : QString(": %1").arg(currentHolderInfo)); +} + +QString LibraryMaintenanceLock::holderInfo() const +{ + return currentHolderInfo; +} + +bool LibraryMaintenanceLock::holderIsRunningLocally() const +{ + return currentHolderIsRunningLocally; +} + static QString fields = "title," "coverPage," @@ -49,8 +360,6 @@ static QString fields = "title," "edited," "read," - "comicVineID," - "hasBeenOpened," "rating," "currentPage," @@ -500,30 +809,46 @@ void DataBaseManagement::exportComicsInfo(QString source, QString dest) { QString connectionName = ""; { - QSqlDatabase destDB = loadDatabaseFromFile(dest); + QFile::remove(dest); + + QString threadId = QString::number((quintptr)QThread::currentThreadId(), 16); + connectionName = dest + threadId; + QSqlDatabase destDB = QSqlDatabase::addDatabase("QSQLITE", connectionName); + destDB.setDatabaseName(dest); + bool success = true; + if (!destDB.open()) { + QLOG_ERROR() << "exportComicsInfo: failed to create export database" << dest << destDB.lastError().text(); + success = false; + } QSqlQuery attach(destDB); - attach.prepare("ATTACH DATABASE '" + QDir().toNativeSeparators(dest) + "' AS dest;"); - attach.exec(); - - QSqlQuery attach2(destDB); - attach2.prepare("ATTACH DATABASE '" + QDir().toNativeSeparators(source) + "' AS source;"); - attach2.exec(); + attach.prepare("ATTACH DATABASE '" + QDir::toNativeSeparators(source) + "' AS source;"); + if (success && !attach.exec()) { + QLOG_ERROR() << "exportComicsInfo: failed to attach source database" << source << attach.lastError().text(); + success = false; + } QSqlQuery queryDBInfo(destDB); - queryDBInfo.prepare("CREATE TABLE dest.db_info (version TEXT NOT NULL)"); - queryDBInfo.exec(); + queryDBInfo.prepare("CREATE TABLE db_info (version TEXT NOT NULL)"); + if (success && !queryDBInfo.exec()) { + QLOG_ERROR() << "exportComicsInfo: failed to create db_info table" << queryDBInfo.lastError().text(); + success = false; + } - QSqlQuery query("INSERT INTO dest.db_info (version) " + QSqlQuery query("INSERT INTO db_info (version) " "VALUES ('" DB_VERSION "')", destDB); - query.exec(); + if (success && !query.exec()) { + QLOG_ERROR() << "exportComicsInfo: failed to write db_info" << query.lastError().text(); + success = false; + } QSqlQuery exportData(destDB); - exportData.prepare("CREATE TABLE dest.comic_info AS SELECT " + fields + + exportData.prepare("CREATE TABLE comic_info AS SELECT " + fields + " FROM source.comic_info WHERE source.comic_info.edited = 1 OR source.comic_info.comicVineID IS NOT NULL"); - exportData.exec(); - connectionName = destDB.connectionName(); + if (success && !exportData.exec()) { + QLOG_ERROR() << "exportComicsInfo: failed to export comic_info" << exportData.lastError().text(); + } } QSqlDatabase::removeDatabase(connectionName); @@ -608,6 +933,11 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) // new 9.5 fields "lastTimeOpened = :lastTimeOpened," + "imageFiltersJson = :imageFiltersJson," + "lastTimeImageFiltersSet = :lastTimeImageFiltersSet," + "lastTimeCoverSet = :lastTimeCoverSet," + "usesExternalCover = :usesExternalCover," + "lastTimeMetadataSet = :lastTimeMetadataSet," //"coverSizeRatio = :coverSizeRatio," //"originalCoverSize = :originalCoverSize," @@ -667,7 +997,13 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) "edited," "comicVineID," "lastTimeOpened," + "imageFiltersJson," + "lastTimeImageFiltersSet," + "lastTimeCoverSet," + "usesExternalCover," + "lastTimeMetadataSet," "coverSizeRatio," + "originalCoverSize," "added," "type," "editor," @@ -682,6 +1018,7 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) "seriesGroup," "mainCharacterOrTeam," "review," + "tags," "hash)" "VALUES (:title," @@ -720,6 +1057,11 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) ":comicVineID," ":lastTimeOpened," + ":imageFiltersJson," + ":lastTimeImageFiltersSet," + ":lastTimeCoverSet," + ":usesExternalCover," + ":lastTimeMetadataSet," ":coverSizeRatio," ":originalCoverSize," @@ -774,6 +1116,7 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) } destDB.commit(); + b = true; for (const auto &hash : hashes) { QSqlQuery getComic(destDB); getComic.prepare("SELECT c.path,ci.coverPage FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) where ci.hash = :hash"); @@ -852,6 +1195,11 @@ void DataBaseManagement::bindValuesFromRecord(const QSqlRecord &record, QSqlQuer bindValue("comicVineID", record, query); bindValue("lastTimeOpened", record, query); + bindValue("imageFiltersJson", record, query); + bindValue("lastTimeImageFiltersSet", record, query); + bindValue("lastTimeCoverSet", record, query); + bindValue("usesExternalCover", record, query); + bindValue("lastTimeMetadataSet", record, query); bindValue("coverSizeRatio", record, query); bindValue("originalCoverSize", record, query); @@ -968,35 +1316,33 @@ int DataBaseManagement::compareVersions(const QString &v1, const QString v2) return 0; } -bool DataBaseManagement::updateToCurrentVersion(const QString &libraryPath) +bool DataBaseManagement::updateToCurrentVersion(const QString &libraryPath, bool maintenanceLockHeld) { - bool pre7 = false; - bool pre7_1 = false; - bool pre8 = false; - bool pre9_5 = false; - bool pre9_8 = false; - bool pre9_13 = false; - bool pre9_14 = false; - bool pre9_16 = false; + std::unique_ptr lock; + if (!maintenanceLockHeld) { + lock = std::make_unique(libraryPath); + if (!lock->tryLock()) { + QLOG_ERROR() << "Database upgrade blocked:" << lock->errorString(); + return false; + } + } QString libraryDatabasePath = LibraryPaths::libraryDatabasePath(libraryPath); + QString backupError; + if (!backupLibrary(libraryPath, DatabaseBackupReason::BeforeUpgrade, &backupError)) { + QLOG_ERROR() << "Database upgrade blocked:" << backupError; + return false; + } - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "7.0.0") < 0) - pre7 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "7.0.3") < 0) - pre7_1 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "8.0.0") < 0) - pre8 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "9.5.0") < 0) - pre9_5 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "9.8.0") < 0) - pre9_8 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "9.13.0") < 0) - pre9_13 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "9.14.0") < 0) - pre9_14 = true; - if (compareVersions(DataBaseManagement::checkValidDB(libraryDatabasePath), "9.16.0") < 0) - pre9_16 = true; + const auto oldVersion = DataBaseManagement::checkValidDB(libraryDatabasePath); + const bool pre7 = compareVersions(oldVersion, "7.0.0") < 0; + const bool pre7_1 = compareVersions(oldVersion, "7.0.3") < 0; + const bool pre8 = compareVersions(oldVersion, "8.0.0") < 0; + const bool pre9_5 = compareVersions(oldVersion, "9.5.0") < 0; + const bool pre9_8 = compareVersions(oldVersion, "9.8.0") < 0; + const bool pre9_13 = compareVersions(oldVersion, "9.13.0") < 0; + const bool pre9_14 = compareVersions(oldVersion, "9.14.0") < 0; + const bool pre9_16 = compareVersions(oldVersion, "9.16.0") < 0; QString connectionName = ""; bool returnValue = true; @@ -1243,6 +1589,400 @@ bool DataBaseManagement::updateToCurrentVersion(const QString &libraryPath) return returnValue; } +bool DataBaseManagement::backupLibrary(const QString &libraryPath, DatabaseBackupReason reason, QString *error, const QString &destinationPath, const QString &protectedBackup) +{ + const auto databasePath = LibraryPaths::libraryDatabasePath(libraryPath); + const auto dataPath = LibraryPaths::libraryDataPath(libraryPath); + QDir backupsDirectory(QDir(dataPath).filePath("backups")); + if (destinationPath.isEmpty() && !backupsDirectory.exists() && !QDir().mkpath(backupsDirectory.path())) { + if (error) + *error = QString("Unable to create backup directory: %1").arg(backupsDirectory.path()); + return false; + } + + if (destinationPath.isEmpty() && (reason == DatabaseBackupReason::AutoUpdate || reason == DatabaseBackupReason::BeforeRepair)) { + const auto automatic = backupsForReason(backupsDirectory, "auto-update"); + const auto maximumAge = reason == DatabaseBackupReason::AutoUpdate ? 24 * 60 * 60 : 60 * 60; + for (const auto &backup : automatic) { + const auto created = backupDateTime(backup); + if (!created.isValid()) + continue; + if (created.secsTo(QDateTime::currentDateTime()) >= maximumAge) + break; + QLOG_INFO() << "Skipping database backup: a recent automatic backup exists" << backup.absoluteFilePath(); + return true; + } + } + + QString sourceVersion; + if (!validateDatabase(databasePath, error, &sourceVersion)) { + QLOG_ERROR() << "Database backup blocked: source database is invalid" << databasePath; + return false; + } + + const auto reasonName = backupReasonName(reason); + const auto finalPath = destinationPath.isEmpty() + ? backupsDirectory.filePath(QString("library-%1-db-%2-%3.ydb").arg(QDateTime::currentDateTime().toString(backupTimestampFormat), sourceVersion, reasonName)) + : QDir::cleanPath(destinationPath); + if (QFileInfo(finalPath).absoluteFilePath() == QFileInfo(databasePath).absoluteFilePath()) { + if (error) + *error = "The backup destination cannot be the live library database"; + return false; + } + + QString backupPath = finalPath; + if (!destinationPath.isEmpty()) { + QTemporaryFile temporary(finalPath + ".XXXXXX.tmp"); + temporary.setAutoRemove(false); + if (!temporary.open()) { + if (error) + *error = temporary.errorString(); + return false; + } + backupPath = temporary.fileName(); + temporary.close(); + temporary.remove(); + } + if (QFile::exists(backupPath)) { + if (error) + *error = QString("Backup file already exists: %1").arg(backupPath); + return false; + } + if (!vacuumDatabaseInto(databasePath, backupPath, error)) { + QFile::remove(backupPath); + QLOG_ERROR() << "Database backup failed" << backupPath << (error ? *error : QString()); + return false; + } + if (!validateDatabase(backupPath, error)) { + QFile::remove(backupPath); + QLOG_ERROR() << "Database backup failed" << backupPath << (error ? *error : QString()); + return false; + } + + if (!destinationPath.isEmpty() && !replaceFileAtomically(backupPath, finalPath, error)) { + QFile::remove(backupPath); + QLOG_ERROR() << "Database backup replacement failed" << finalPath << (error ? *error : QString()); + return false; + } + + QLOG_INFO() << "Database backup created" << finalPath; + if (destinationPath.isEmpty()) + removeOldBackups(backupsDirectory, reason, finalPath, protectedBackup); + return true; +} + +DatabaseRestoreResult DataBaseManagement::restoreLibrary(const QString &libraryPath, const QString &backupPath, bool allowInvalidCurrent, bool removeStaleLock) +{ + DatabaseRestoreResult result; + QString selectedVersion; + if (!validateDatabase(backupPath, &result.error, &selectedVersion)) { + result.status = DatabaseRestoreStatus::InvalidBackup; + return result; + } + if (compareVersions(selectedVersion, DB_VERSION) > 0) { + result.status = DatabaseRestoreStatus::NewerBackup; + result.error = QString("The backup database version %1 is newer than supported version %2").arg(selectedVersion, DB_VERSION); + return result; + } + + if (!QDir(libraryPath).exists()) { + result.status = DatabaseRestoreStatus::Failed; + result.error = QString("Library folder does not exist: %1").arg(libraryPath); + return result; + } + if (!QDir().mkpath(LibraryPaths::libraryDataPath(libraryPath))) { + result.status = DatabaseRestoreStatus::Failed; + result.error = QString("Unable to create library metadata folder: %1").arg(LibraryPaths::libraryDataPath(libraryPath)); + return result; + } + + LibraryMaintenanceLock lock(libraryPath); + if (!lock.tryLock(removeStaleLock)) { + result.status = DatabaseRestoreStatus::LockFailed; + result.error = lock.errorString(); + result.lockHolderInfo = lock.holderInfo(); + result.lockHolderIsRunningLocally = lock.holderIsRunningLocally(); + return result; + } + + if (!recoverInterruptedRestore(libraryPath, &result.error, true)) { + result.status = DatabaseRestoreStatus::Failed; + return result; + } + + const auto databasePath = LibraryPaths::libraryDatabasePath(libraryPath); + const auto stagedPath = stagedDatabasePath(databasePath); + const auto rollbackPath = rollbackDatabasePath(databasePath); + const auto markerPath = restoreMarkerPath(databasePath); + const bool hadOriginal = QFile::exists(databasePath); + + QString currentVersion; + const bool currentValid = hadOriginal && validateDatabase(databasePath, nullptr, ¤tVersion); + if (hadOriginal && !currentValid && !allowInvalidCurrent) { + result.status = DatabaseRestoreStatus::InvalidCurrentDatabase; + result.error = "The current library database is invalid; confirmation is required before replacing it"; + return result; + } + + if (currentValid && !backupLibrary(libraryPath, DatabaseBackupReason::BeforeRestore, &result.error, { }, backupPath)) { + result.status = DatabaseRestoreStatus::Failed; + return result; + } + + removeDatabaseUnit(stagedPath); + removeDatabaseUnit(rollbackPath); + QFile::remove(markerPath); + if (!QFile::copy(backupPath, stagedPath)) { + result.status = DatabaseRestoreStatus::Failed; + result.error = QString("Unable to stage backup: %1").arg(backupPath); + return result; + } + + QString stagedVersion; + if (!validateDatabase(stagedPath, &result.error, &stagedVersion)) { + removeDatabaseUnit(stagedPath); + result.status = DatabaseRestoreStatus::InvalidBackup; + return result; + } + if (compareVersions(stagedVersion, DB_VERSION) > 0) { + removeDatabaseUnit(stagedPath); + result.status = DatabaseRestoreStatus::NewerBackup; + result.error = QString("The staged database version %1 is newer than supported version %2").arg(stagedVersion, DB_VERSION); + return result; + } + + if (!writeRestoreMarker(markerPath, hadOriginal, "prepared", &result.error)) { + removeDatabaseUnit(stagedPath); + result.status = DatabaseRestoreStatus::Failed; + return result; + } + + auto rollBack = [&]() { + if (!removeDatabaseUnit(databasePath)) + return false; + if (hadOriginal && !moveDatabaseUnit(rollbackPath, databasePath, &result.error)) + return false; + if (!removeDatabaseUnit(stagedPath) || !removeDatabaseUnit(rollbackPath)) { + result.error = "The original database was restored, but restore cleanup failed"; + return false; + } + if (!QFile::remove(markerPath)) { + result.error = "The original database was restored, but the restore marker could not be removed"; + return false; + } + return true; + }; + + if (hadOriginal && !moveDatabaseUnit(databasePath, rollbackPath, &result.error)) { + removeDatabaseUnit(stagedPath); + QFile::remove(markerPath); + result.status = DatabaseRestoreStatus::Failed; + return result; + } + if (!writeRestoreMarker(markerPath, hadOriginal, "original-moved", &result.error)) { + result.status = rollBack() ? DatabaseRestoreStatus::Failed : DatabaseRestoreStatus::RollbackFailed; + return result; + } + if (!moveDatabaseUnit(stagedPath, databasePath, &result.error)) { + result.status = rollBack() ? DatabaseRestoreStatus::Failed : DatabaseRestoreStatus::RollbackFailed; + return result; + } + if (!writeRestoreMarker(markerPath, hadOriginal, "installed", &result.error)) { + result.status = rollBack() ? DatabaseRestoreStatus::Failed : DatabaseRestoreStatus::RollbackFailed; + return result; + } + + if (compareVersions(stagedVersion, DB_VERSION) < 0) { + if (!updateToCurrentVersion(libraryPath, true)) { + result.error = "Unable to upgrade the restored database"; + result.status = rollBack() ? DatabaseRestoreStatus::Failed : DatabaseRestoreStatus::RollbackFailed; + return result; + } + result.upgraded = true; + result.restoredVersion = DB_VERSION; + } else { + result.restoredVersion = stagedVersion; + } + + QFile::remove(markerPath); + removeDatabaseUnit(rollbackPath); + removeDatabaseUnit(stagedPath); + result.status = DatabaseRestoreStatus::Success; + return result; +} + +bool DataBaseManagement::recoverInterruptedRestore(const QString &libraryPath, QString *error, bool maintenanceLockHeld) +{ + const auto databasePath = LibraryPaths::libraryDatabasePath(libraryPath); + const auto stagedPath = stagedDatabasePath(databasePath); + const auto rollbackPath = rollbackDatabasePath(databasePath); + const auto markerPath = restoreMarkerPath(databasePath); + if (!QFile::exists(markerPath) && !QFile::exists(stagedPath) && !QFile::exists(rollbackPath)) + return true; + + std::unique_ptr lock; + if (!maintenanceLockHeld) { + lock = std::make_unique(libraryPath); + if (!lock->tryLock()) { + if (error) + *error = lock->errorString(); + return false; + } + } + + if (!QFile::exists(markerPath)) { + removeDatabaseUnit(stagedPath); + if (QFile::exists(databasePath)) + removeDatabaseUnit(rollbackPath); + else if (QFile::exists(rollbackPath) && !moveDatabaseUnit(rollbackPath, databasePath, error)) + return false; + return true; + } + + const bool hadOriginal = markerHadOriginal(markerPath); + if (QFile::exists(rollbackPath)) { + if (!removeDatabaseUnit(databasePath) || !moveDatabaseUnit(rollbackPath, databasePath, error)) + return false; + } else if (!hadOriginal) { + if (!QFile::exists(databasePath)) { + removeDatabaseUnit(stagedPath); + QFile::remove(markerPath); + if (error) + *error = "Interrupted restore did not install a database; retry the restore"; + return false; + } + QString ignoredVersion; + if (!validateDatabase(databasePath, error, &ignoredVersion)) { + removeDatabaseUnit(stagedPath); + removeDatabaseUnit(rollbackPath); + QFile::remove(markerPath); + if (error) + error->clear(); + return true; + } + } else if (hadOriginal && !QFile::exists(databasePath)) { + removeDatabaseUnit(stagedPath); + QFile::remove(markerPath); + if (error) + error->clear(); + return true; + } + + removeDatabaseUnit(stagedPath); + removeDatabaseUnit(rollbackPath); + QFile::remove(markerPath); + return true; +} + +bool DataBaseManagement::prepareForRecreation(const QString &libraryPath, QString *error, bool maintenanceLockHeld) +{ + if (!recoverInterruptedRestore(libraryPath, error, maintenanceLockHeld)) + return false; + + const auto databasePath = LibraryPaths::libraryDatabasePath(libraryPath); + bool success = removeDatabaseUnit(databasePath); + success = removeDatabaseUnit(stagedDatabasePath(databasePath)) && success; + success = removeDatabaseUnit(rollbackDatabasePath(databasePath)) && success; + success = removeDatabaseUnit(salvageDatabasePath(databasePath)) && success; + success = (!QFile::exists(restoreMarkerPath(databasePath)) || QFile::remove(restoreMarkerPath(databasePath))) && success; + + QDir covers(LibraryPaths::libraryCoversFolderPath(libraryPath)); + success = (!covers.exists() || covers.removeRecursively()) && success; + const auto idPath = LibraryPaths::idPath(libraryPath); + success = (!QFile::exists(idPath) || QFile::remove(idPath)) && success; + if (!success && error) + *error = QString("Unable to prepare library metadata for recreation: %1").arg(LibraryPaths::libraryDataPath(libraryPath)); + return success; +} + +QFileInfoList DataBaseManagement::libraryBackups(const QString &libraryPath) +{ + return QDir(QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("backups")) + .entryInfoList({ "library-*.ydb" }, QDir::Files, QDir::Name | QDir::Reversed); +} + +bool DataBaseManagement::isLibraryDatabaseValid(const QString &libraryPath) +{ + return validateDatabase(LibraryPaths::libraryDatabasePath(libraryPath), nullptr); +} + +DatabaseSalvageResult DataBaseManagement::salvageLibrary(const QString &libraryPath, bool removeStaleLock) +{ + DatabaseSalvageResult result; + const auto databasePath = LibraryPaths::libraryDatabasePath(libraryPath); + if (!QFile::exists(databasePath)) { + result.error = QString("Library database not found: %1").arg(databasePath); + return result; + } + if (validateDatabase(databasePath, nullptr)) { + result.status = DatabaseSalvageStatus::AlreadyValid; + return result; + } + + const auto salvagePath = salvageDatabasePath(databasePath); + { + LibraryMaintenanceLock lock(libraryPath); + if (!lock.tryLock(removeStaleLock)) { + result.status = DatabaseSalvageStatus::LockFailed; + result.error = lock.errorString(); + result.lockHolderInfo = lock.holderInfo(); + result.lockHolderIsRunningLocally = lock.holderIsRunningLocally(); + return result; + } + + result.preservedDatabasePath = damagedDatabasePath(libraryPath); + if (!copyDatabaseUnit(databasePath, result.preservedDatabasePath, &result.error)) { + result.preservedDatabasePath.clear(); + return result; + } + + // index corruption can be repaired in place without losing anything + QString connectionName; + { + auto db = loadDatabaseFromFile(databasePath); + connectionName = db.connectionName(); + if (db.isOpen()) { + QSqlQuery reindex(db); + if (!reindex.exec("REINDEX")) + QLOG_INFO() << "REINDEX did not complete during salvage:" << reindex.lastError().text(); + } + } + if (!connectionName.isEmpty()) + QSqlDatabase::removeDatabase(connectionName); + + if (validateDatabase(databasePath, nullptr)) { + QLOG_INFO() << "Library database salvaged with REINDEX" << databasePath; + result.status = DatabaseSalvageStatus::Reindexed; + return result; + } + + // rebuild the reachable logical content into a fresh file; this fails if + // table pages themselves are unreadable, in which case only restoring a + // backup or recreating the library can help + QFile::remove(salvagePath); + QString rebuildError; + if (!vacuumDatabaseInto(databasePath, salvagePath, &rebuildError) || !validateDatabase(salvagePath, &rebuildError)) { + QFile::remove(salvagePath); + result.error = QString("The library database could not be repaired: %1").arg(rebuildError); + QLOG_ERROR() << "Library database salvage failed" << databasePath << rebuildError; + return result; + } + } + + // the rebuilt database is installed through the restore machinery to get its + // staging, rollback, and crash-recovery guarantees; restoreLibrary acquires + // the maintenance lock itself, so it is released above + const auto restoreResult = restoreLibrary(libraryPath, salvagePath, true); + QFile::remove(salvagePath); + if (!restoreResult.success()) { + result.error = restoreResult.error; + return result; + } + QLOG_INFO() << "Library database salvaged by rebuilding it" << databasePath; + result.status = DatabaseSalvageStatus::Rebuilt; + return result; +} + DatabaseAccess DataBaseManagement::getDatabaseAccess(const QString &libraryPath) { DatabaseAccess access = { false, false, false, false }; diff --git a/YACReaderLibrary/db/data_base_management.h b/YACReaderLibrary/db/data_base_management.h index 1e170afd5..73db5b780 100644 --- a/YACReaderLibrary/db/data_base_management.h +++ b/YACReaderLibrary/db/data_base_management.h @@ -4,6 +4,8 @@ #include #include +#include + class ComicsInfoExporter : public QThread { Q_OBJECT @@ -47,6 +49,78 @@ struct DatabaseAccess { } }; +enum class DatabaseBackupReason { + AutoUpdate, + BeforeUpgrade, + BeforeRepair, + BeforeRestore, + Manual +}; + +class LibraryMaintenanceLock +{ +public: + explicit LibraryMaintenanceLock(const QString &libraryPath); + ~LibraryMaintenanceLock(); + + bool tryLock(bool removeStaleLock = false); + QString errorString() const; + QString holderInfo() const; + bool holderIsRunningLocally() const; + +private: + bool tryLockFile(QLockFile &lock, bool removeStaleLock); + void captureLockInfo(QLockFile &lock); + + std::unique_ptr maintenanceLock; + std::unique_ptr legacyRepairLock; + QString failedLockPath; + QString currentHolderInfo; + bool currentHolderIsRunningLocally { false }; +}; + +enum class DatabaseRestoreStatus { + Success, + InvalidBackup, + NewerBackup, + InvalidCurrentDatabase, + LockFailed, + Failed, + RollbackFailed +}; + +struct DatabaseRestoreResult { + DatabaseRestoreStatus status { DatabaseRestoreStatus::Failed }; + QString error; + QString restoredVersion; + QString lockHolderInfo; + bool lockHolderIsRunningLocally { false }; + bool upgraded { false }; + + bool success() const { return status == DatabaseRestoreStatus::Success; } +}; + +enum class DatabaseSalvageStatus { + AlreadyValid, + Reindexed, + Rebuilt, + LockFailed, + Failed +}; + +struct DatabaseSalvageResult { + DatabaseSalvageStatus status { DatabaseSalvageStatus::Failed }; + QString error; + QString preservedDatabasePath; + QString lockHolderInfo; + bool lockHolderIsRunningLocally { false }; + + bool success() const + { + return status == DatabaseSalvageStatus::AlreadyValid || status == DatabaseSalvageStatus::Reindexed || status == DatabaseSalvageStatus::Rebuilt; + } +}; + class DataBaseManagement : public QObject { Q_OBJECT @@ -76,7 +150,14 @@ class DataBaseManagement : public QObject static QString checkValidDB(const QString &fullPath); // retorna "" si la DB es inválida ó la versión si es válida. static int compareVersions(const QString &v1, const QString v2); // retorna <0 si v1 < v2, 0 si v1 = v2 y >0 si v1 > v2 - static bool updateToCurrentVersion(const QString &libraryPath); + static bool updateToCurrentVersion(const QString &libraryPath, bool maintenanceLockHeld = false); + static bool backupLibrary(const QString &libraryPath, DatabaseBackupReason reason, QString *error = nullptr, const QString &destinationPath = { }, const QString &protectedBackup = { }); + static DatabaseRestoreResult restoreLibrary(const QString &libraryPath, const QString &backupPath, bool allowInvalidCurrent = false, bool removeStaleLock = false); + static bool recoverInterruptedRestore(const QString &libraryPath, QString *error = nullptr, bool maintenanceLockHeld = false); + static bool prepareForRecreation(const QString &libraryPath, QString *error = nullptr, bool maintenanceLockHeld = false); + static QFileInfoList libraryBackups(const QString &libraryPath); + static bool isLibraryDatabaseValid(const QString &libraryPath); + static DatabaseSalvageResult salvageLibrary(const QString &libraryPath, bool removeStaleLock = false); static DatabaseAccess getDatabaseAccess(const QString &libraryPath); }; diff --git a/YACReaderLibrary/db/query_parser.cpp b/YACReaderLibrary/db/query_parser.cpp index e79cde4f2..99dcd1a2f 100644 --- a/YACReaderLibrary/db/query_parser.cpp +++ b/YACReaderLibrary/db/query_parser.cpp @@ -6,19 +6,6 @@ #include #include -const std::map> QueryParser::fieldNames { - { FieldType::numeric, { "numpages", "count", "arccount", "alternateCount", "rating" } }, - { FieldType::text, { "date", "number", "arcnumber", "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes", "editor", "imprint", "teams", "locations", "series", "alternateSeries", "alternateNumber", "languageISO", "seriesGroup", "mainCharacterOrTeam", "review", "tags" } }, - { FieldType::boolean, { "color", "read", "edited", "hasBeenOpened" } }, - { FieldType::date, { "added", "lastTimeOpened" } }, - { FieldType::dateFolder, { "added", "updated" } }, - { FieldType::filename, { "filename" } }, - { FieldType::folder, { "folder" } }, - { FieldType::booleanFolder, { "completed", "finished" } }, - { FieldType::enumField, { "type" } }, - { FieldType::enumFieldFolder, { "foldertype" } } -}; - std::string operatorToSQLOperator(const std::string &expOperator) { if (expOperator == ":" || expOperator == "=" || expOperator == "==") { @@ -45,24 +32,24 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi ++bindPosition; if (toLower(children[0].t) == "all") { sqlString += "("; - for (const auto &field : fieldNames.at(FieldType::text)) { + for (const auto &field : searchFieldNames().at(FieldType::Text)) { sqlString += "UPPER(ci." + field + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR "; } sqlString += "UPPER(c.filename) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR "; sqlString += "UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; - } else if (isIn(fieldType(children[0].t), { FieldType::numeric, FieldType::date })) { + } else if (isIn(fieldType(children[0].t), { FieldType::Numeric, FieldType::Date })) { sqlString += "ci." + children[0].t + " " + operatorToSQLOperator(expOperator) + " :bindPosition" + std::to_string(bindPosition) + " "; - } else if (isIn(fieldType(children[0].t), { FieldType::dateFolder })) { + } else if (isIn(fieldType(children[0].t), { FieldType::DateFolder })) { sqlString += "f." + children[0].t + " " + operatorToSQLOperator(expOperator) + " :bindPosition" + std::to_string(bindPosition) + " "; - } else if (isIn(fieldType(children[0].t), { FieldType::boolean, FieldType::enumField })) { + } else if (isIn(fieldType(children[0].t), { FieldType::Boolean, FieldType::EnumField })) { sqlString += "ci." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; - } else if (fieldType(children[0].t) == FieldType::filename) { + } else if (fieldType(children[0].t) == FieldType::Filename) { sqlString += "(UPPER(c." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; - } else if (fieldType(children[0].t) == FieldType::folder) { + } else if (fieldType(children[0].t) == FieldType::Folder) { sqlString += "(UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; - } else if (fieldType(children[0].t) == FieldType::booleanFolder) { + } else if (fieldType(children[0].t) == FieldType::BooleanFolder) { sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; - } else if (fieldType(children[0].t) == FieldType::enumFieldFolder) { + } else if (fieldType(children[0].t) == FieldType::EnumFieldFolder) { if (children[0].t == "foldertype") { sqlString += "f.type = :bindPosition" + std::to_string(bindPosition) + " "; } else { @@ -99,9 +86,9 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) { if (t == "expression") { std::string bind_string(":bindPosition" + std::to_string(++bindPosition)); - if (isIn(fieldType(children[0].t), { FieldType::numeric })) { + if (isIn(fieldType(children[0].t), { FieldType::Numeric })) { selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t)); - } else if (isIn(fieldType(children[0].t), { FieldType::boolean, FieldType::booleanFolder })) { + } else if (isIn(fieldType(children[0].t), { FieldType::Boolean, FieldType::BooleanFolder })) { auto value = toLower(children[1].t); if (value == "true") { selectQuery.bindValue(QString::fromStdString(bind_string), 1); @@ -110,7 +97,7 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) } else { selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(value)); } - } else if ((isIn(fieldType(children[0].t), { FieldType::enumField, FieldType::enumFieldFolder }))) { + } else if ((isIn(fieldType(children[0].t), { FieldType::EnumField, FieldType::EnumFieldFolder }))) { auto enumType = children[0].t; auto value = toLower(children[1].t); if (enumType == "type" || enumType == "foldertype") { @@ -128,7 +115,7 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) } else { selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t)); } - } else if ((isIn(fieldType(children[0].t), { FieldType::date, FieldType::dateFolder }))) { + } else if ((isIn(fieldType(children[0].t), { FieldType::Date, FieldType::DateFolder }))) { selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString(parseDate(children[1].t, expOperator))); } else { if (expOperator == "=" || expOperator == ":" || expOperator == "") { @@ -230,13 +217,7 @@ bool QueryParser::isOperatorToken(Token::Type type) QueryParser::FieldType QueryParser::fieldType(const std::string &str) { - for (const auto &names : fieldNames) { - if (std::find(names.second.begin(), names.second.end(), toLower(str)) != names.second.end()) { - return names.first; - } - } - - return FieldType::unknown; + return searchFieldType(str); } std::string QueryParser::join(const QStringList &strings, const std::string &delim) diff --git a/YACReaderLibrary/db/query_parser.h b/YACReaderLibrary/db/query_parser.h index 1e1f5ee5d..20b4e0874 100644 --- a/YACReaderLibrary/db/query_parser.h +++ b/YACReaderLibrary/db/query_parser.h @@ -2,6 +2,7 @@ #define QUERY_PARSER_H #include "query_lexer.h" +#include "search_field_registry.h" #include @@ -84,17 +85,7 @@ class QueryParser bool isOperatorToken(Token::Type type); - enum class FieldType { unknown, - numeric, - text, - boolean, - date, - dateFolder, - folder, - booleanFolder, - filename, - enumField, - enumFieldFolder }; + using FieldType = SearchFieldType; static FieldType fieldType(const std::string &str); static std::string join(const QStringList &strings, const std::string &delim); @@ -106,8 +97,6 @@ class QueryParser TreeNode locationExpression(); TreeNode expression(); TreeNode baseToken(); - - static const std::map> fieldNames; }; #endif // QUERY_PARSER_H diff --git a/YACReaderLibrary/db/search_field_registry.cpp b/YACReaderLibrary/db/search_field_registry.cpp new file mode 100644 index 000000000..338e29296 --- /dev/null +++ b/YACReaderLibrary/db/search_field_registry.cpp @@ -0,0 +1,157 @@ +#include "search_field_registry.h" + +#include + +#include +#include + +namespace { + +QString inputDescription(SearchFieldType type) +{ + switch (type) { + case SearchFieldType::Text: + case SearchFieldType::Filename: + case SearchFieldType::Folder: + return QCoreApplication::translate("SearchFieldRegistry", "Text, quoted text"); + case SearchFieldType::Numeric: + return QCoreApplication::translate("SearchFieldRegistry", "Integer"); + case SearchFieldType::Boolean: + case SearchFieldType::BooleanFolder: + return QCoreApplication::translate("SearchFieldRegistry", "Boolean (true / false)"); + case SearchFieldType::Date: + case SearchFieldType::DateFolder: + return QCoreApplication::translate("SearchFieldRegistry", "Integer (number of days)"); + case SearchFieldType::EnumField: + case SearchFieldType::EnumFieldFolder: + return QCoreApplication::translate("SearchFieldRegistry", "Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma)"); + case SearchFieldType::Unknown: + return { }; + } + + return { }; +} + +SearchFieldDefinition field( + const char *key, + const char *displayName, + const char *description, + const char *example, + SearchFieldType type, + SearchFieldCategory category, + SearchFieldScope scope = SearchFieldScope::Comics) +{ + return { + QString::fromLatin1(key), + QString::fromUtf8(displayName), + QCoreApplication::translate("SearchFieldRegistry", description), + inputDescription(type), + QString::fromLatin1(example), + type, + category, + scope + }; +} + +} + +const std::map> &searchFieldNames() +{ + static const std::map> names { + { SearchFieldType::Numeric, { "numpages", "count", "arccount", "alternateCount", "rating" } }, + { SearchFieldType::Text, { "date", "number", "arcnumber", "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes", "editor", "imprint", "teams", "locations", "series", "alternateSeries", "alternateNumber", "languageISO", "seriesGroup", "mainCharacterOrTeam", "review", "tags" } }, + { SearchFieldType::Boolean, { "color", "read", "edited", "hasBeenOpened" } }, + { SearchFieldType::Date, { "added", "lastTimeOpened" } }, + { SearchFieldType::DateFolder, { "added", "updated" } }, + { SearchFieldType::Filename, { "filename" } }, + { SearchFieldType::Folder, { "folder" } }, + { SearchFieldType::BooleanFolder, { "completed", "finished" } }, + { SearchFieldType::EnumField, { "type" } }, + { SearchFieldType::EnumFieldFolder, { "foldertype" } } + }; + + return names; +} + +SearchFieldType searchFieldType(const std::string &name) +{ + std::string lowerName(name); + std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower); + + for (const auto &[type, names] : searchFieldNames()) { + for (const auto &candidate : names) { + std::string lowerCandidate(candidate); + std::transform(lowerCandidate.begin(), lowerCandidate.end(), lowerCandidate.begin(), ::tolower); + if (lowerCandidate == lowerName) + return type; + } + } + + return SearchFieldType::Unknown; +} + +const QList &searchFieldDefinitions() +{ + using Category = SearchFieldCategory; + using Scope = SearchFieldScope; + using Type = SearchFieldType; + + static const QList definitions { + field("title", "Title", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic title"), "title:Moonbound", Type::Text, Category::Common), + field("series", "Series", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Series name"), "series:\"Starfall Chronicles\"", Type::Text, Category::Common), + field("number", "Number", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Issue number"), "number>=10", Type::Text, Category::Common), + field("volume", "Volume", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Volume identifier"), "volume:2", Type::Text, Category::Common), + field("type", "Comic type", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Reading format"), "type:manga", Type::EnumField, Category::Common), + field("rating", "Rating", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic rating"), "rating>=4", Type::Numeric, Category::Common), + field("tags", "Tags", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Textual tags"), "tags:\"to review\"", Type::Text, Category::Common), + + field("writer", "Writer", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Writer credit"), "writer:Smith", Type::Text, Category::Credits), + field("penciller", "Penciller", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Penciller credit"), "penciller:\"Alex Smith\"", Type::Text, Category::Credits), + field("inker", "Inker", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Inker credit"), "inker:\"Taylor Reed\"", Type::Text, Category::Credits), + field("colorist", "Colorist", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Colorist credit"), "colorist:\"Morgan Lane\"", Type::Text, Category::Credits), + field("letterer", "Letterer", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Letterer credit"), "letterer:\"Casey Brooks\"", Type::Text, Category::Credits), + field("coverartist", "Cover artist", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Cover artist credit"), "coverartist:\"Jordan Blake\"", Type::Text, Category::Credits), + field("editor", "Editor", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Editor credit"), "editor:\"Avery Stone\"", Type::Text, Category::Credits), + + field("storyarc", "Story arc", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Story arc name"), "storyarc:Afterlight", Type::Text, Category::Story), + field("arcnumber", "Arc number", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Position within a story arc"), "arcnumber>=2", Type::Text, Category::Story), + field("arccount", "Arc count", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Number of issues in a story arc"), "arccount>5", Type::Numeric, Category::Story), + field("characters", "Characters", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Characters appearing in the comic"), "characters:Solara", Type::Text, Category::Story), + field("teams", "Teams", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Teams appearing in the comic"), "teams:\"Aurora Guard\"", Type::Text, Category::Story), + field("locations", "Locations", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Locations appearing in the comic"), "locations:Greyhaven", Type::Text, Category::Story), + field("mainCharacterOrTeam", "Main character or team", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Primary character or team"), "mainCharacterOrTeam:Solara", Type::Text, Category::Story), + field("synopsis", "Synopsis", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic synopsis"), "synopsis:\"parallel world\"", Type::Text, Category::Story), + + field("publisher", "Publisher", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Publisher name"), "publisher:ExamplePress", Type::Text, Category::Publication), + field("imprint", "Imprint", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Publishing imprint"), "imprint:\"Silver Line\"", Type::Text, Category::Publication), + field("format", "Format", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Publication format"), "format:annual", Type::Text, Category::Publication), + field("agerating", "Age rating", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Recommended age rating"), "agerating:Teen", Type::Text, Category::Publication), + field("genere", "Genre", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic genre"), "genere:Horror", Type::Text, Category::Publication), + field("languageISO", "Language", QT_TRANSLATE_NOOP("SearchFieldRegistry", "ISO language code"), "languageISO:en", Type::Text, Category::Publication), + field("date", "Publication date", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Publication date metadata"), "date:2024", Type::Text, Category::Publication), + field("seriesGroup", "Series group", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Series grouping metadata"), "seriesGroup:\"Pocket Editions\"", Type::Text, Category::Publication), + field("alternateSeries", "Alternate series", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Alternate series name"), "alternateSeries:\"Midnight Tales\"", Type::Text, Category::Publication), + field("alternateNumber", "Alternate number", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Alternate issue number"), "alternateNumber>=10", Type::Text, Category::Publication), + field("alternateCount", "Alternate count", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Alternate series issue count"), "alternateCount>20", Type::Numeric, Category::Publication), + field("count", "Issue count", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Number of issues in the series"), "count>=12", Type::Numeric, Category::Publication), + + field("read", "Read", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether the comic is marked as read"), "read:false", Type::Boolean, Category::ReadingAndFiles), + field("hasBeenOpened", "Has been opened", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether reading has started"), "hasBeenOpened:true", Type::Boolean, Category::ReadingAndFiles), + field("edited", "Edited", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether metadata has been edited"), "edited:true", Type::Boolean, Category::ReadingAndFiles), + field("color", "Color", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether the comic is in color"), "color:true", Type::Boolean, Category::ReadingAndFiles), + field("numpages", "Page count", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Number of pages"), "numpages>100", Type::Numeric, Category::ReadingAndFiles), + field("filename", "File name", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic file name"), "filename:cbz", Type::Filename, Category::ReadingAndFiles), + field("added", "Date added", QT_TRANSLATE_NOOP("SearchFieldRegistry", "When the item was added"), "added>7", Type::Date, Category::ReadingAndFiles, Scope::ComicsAndFolders), + field("lastTimeOpened", "Last opened", QT_TRANSLATE_NOOP("SearchFieldRegistry", "When the comic was last opened"), "lastTimeOpened>30", Type::Date, Category::ReadingAndFiles), + field("notes", "Notes", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Comic notes"), "notes:\"variant cover\"", Type::Text, Category::ReadingAndFiles), + field("review", "Review", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Review text"), "review:\"highly recommended\"", Type::Text, Category::ReadingAndFiles), + + field("folder", "Folder", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Parent folder name"), "folder:\"Example Comics\"", Type::Folder, Category::Folders, Scope::Folders), + field("foldertype", "Folder type", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Default reading format for the folder"), "foldertype:manga", Type::EnumFieldFolder, Category::Folders, Scope::Folders), + field("completed", "Completed", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether the folder is complete"), "completed:true", Type::BooleanFolder, Category::Folders, Scope::Folders), + field("finished", "Finished", QT_TRANSLATE_NOOP("SearchFieldRegistry", "Whether the folder is marked as finished"), "finished:true", Type::BooleanFolder, Category::Folders, Scope::Folders), + field("updated", "Date updated", QT_TRANSLATE_NOOP("SearchFieldRegistry", "When the folder was updated"), "updated>7", Type::DateFolder, Category::Folders, Scope::Folders) + }; + + return definitions; +} diff --git a/YACReaderLibrary/db/search_field_registry.h b/YACReaderLibrary/db/search_field_registry.h new file mode 100644 index 000000000..62bba2339 --- /dev/null +++ b/YACReaderLibrary/db/search_field_registry.h @@ -0,0 +1,55 @@ +#ifndef SEARCH_FIELD_REGISTRY_H +#define SEARCH_FIELD_REGISTRY_H + +#include +#include + +#include +#include +#include + +enum class SearchFieldType { + Unknown, + Numeric, + Text, + Boolean, + Date, + DateFolder, + Folder, + BooleanFolder, + Filename, + EnumField, + EnumFieldFolder +}; + +enum class SearchFieldCategory { + Common, + Credits, + Story, + Publication, + ReadingAndFiles, + Folders +}; + +enum class SearchFieldScope { + Comics, + Folders, + ComicsAndFolders +}; + +struct SearchFieldDefinition { + QString key; + QString displayName; + QString description; + QString input; + QString example; + SearchFieldType type; + SearchFieldCategory category; + SearchFieldScope scope; +}; + +const std::map> &searchFieldNames(); +SearchFieldType searchFieldType(const std::string &name); +const QList &searchFieldDefinitions(); + +#endif // SEARCH_FIELD_REGISTRY_H diff --git a/YACReaderLibrary/db_helper.cpp b/YACReaderLibrary/db_helper.cpp index f250d80e0..469c05f2e 100644 --- a/YACReaderLibrary/db_helper.cpp +++ b/YACReaderLibrary/db_helper.cpp @@ -64,15 +64,7 @@ QList DBHelper::getFolderComicsFromLibraryForReading(qulonglong l auto c1 = static_cast(i1); auto c2 = static_cast(i2); - if (c1->info.number.isNull() && c2->info.number.isNull()) { - return naturalSortLessThanCI(c1->name, c2->name); - } else { - if (c1->info.number.isNull() == false && c2->info.number.isNull() == false) { - return naturalSortLessThanCI(c1->info.number.toString(), c2->info.number.toString()); - } else { - return c2->info.number.isNull(); - } - } + return comicNumberLessThan(c1->info.number, c1->name, c2->info.number, c2->name); }); return list; @@ -1702,15 +1694,7 @@ QList DBHelper::getSortedComicsFromParent(qulonglong parentId, QSqlData } std::sort(list.begin(), list.end(), [](const ComicDB &c1, const ComicDB &c2) { - if (c1.info.number.isNull() && c2.info.number.isNull()) { - return naturalSortLessThanCI(c1.name, c2.name); - } else { - if (c1.info.number.isNull() == false && c2.info.number.isNull() == false) { - return naturalSortLessThanCI(c1.info.number.toString(), c2.info.number.toString()); - } else { - return c2.info.number.isNull(); - } - } + return comicNumberLessThan(c1.info.number, c1.name, c2.info.number, c2.name); }); // selectQuery.finish(); diff --git a/YACReaderLibrary/grid_comics_view.cpp b/YACReaderLibrary/grid_comics_view.cpp index ff25d753b..970b66d63 100644 --- a/YACReaderLibrary/grid_comics_view.cpp +++ b/YACReaderLibrary/grid_comics_view.cpp @@ -358,11 +358,13 @@ void GridComicsView::updateCoversSizeInContext(int width, QQmlContext *ctxt) { int cellBottomMarging = 8 * (1 + 2 * (1 - (float(YACREADER_MAX_GRID_ZOOM_WIDTH - width) / (YACREADER_MAX_GRID_ZOOM_WIDTH - YACREADER_MIN_GRID_ZOOM_WIDTH)))); - ctxt->setContextProperty("cellCustomHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + 51 + cellBottomMarging); + int infoHeight = 56; + + ctxt->setContextProperty("cellCustomHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + infoHeight + cellBottomMarging); ctxt->setContextProperty("cellCustomWidth", (width * YACREADER_MIN_CELL_CUSTOM_WIDTH) / YACREADER_MIN_COVER_WIDTH); ctxt->setContextProperty("itemWidth", width); - ctxt->setContextProperty("itemHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + 51); + ctxt->setContextProperty("itemHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + infoHeight); ctxt->setContextProperty("coverWidth", width); ctxt->setContextProperty("coverHeight", (width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH); diff --git a/YACReaderLibrary/import_widget.cpp b/YACReaderLibrary/import_widget.cpp index 522e46568..335ccda12 100644 --- a/YACReaderLibrary/import_widget.cpp +++ b/YACReaderLibrary/import_widget.cpp @@ -358,6 +358,18 @@ void ImportWidget::setXMLScanLook() hideButton->setVisible(false); } +void ImportWidget::setRepairLook() +{ + iconLabel->setPixmap(theme.importWidget.updatingIcon); + text->setText(QCoreApplication::translate("LibraryWindowActions", "Repair covers and comic info")); + textDescription->setText(tr("

The current library is being checked for missing covers and incomplete comic information.

This can take several minutes. You can stop the process and run it again later.

")); + + stopButton->setVisible(true); + coversLabel->setVisible(false); + coversViewContainer->setVisible(false); + hideButton->setVisible(false); +} + void ImportWidget::clearScene() { } diff --git a/YACReaderLibrary/import_widget.h b/YACReaderLibrary/import_widget.h index 9da45b84f..6579a8b27 100644 --- a/YACReaderLibrary/import_widget.h +++ b/YACReaderLibrary/import_widget.h @@ -37,6 +37,7 @@ public slots: void setUpdateLook(); void setUpgradeLook(); void setXMLScanLook(); + void setRepairLook(); void showCovers(bool hide); private: diff --git a/YACReaderLibrary/initial_comic_info_extractor.cpp b/YACReaderLibrary/initial_comic_info_extractor.cpp index 9cdef9189..5b9b47da2 100644 --- a/YACReaderLibrary/initial_comic_info_extractor.cpp +++ b/YACReaderLibrary/initial_comic_info_extractor.cpp @@ -3,17 +3,20 @@ #include "comic.h" #include "compressed_archive.h" #include "cover_utils.h" +#include "epub_page_index.h" #include "pdf_comic.h" #include "qnaturalsorting.h" #include +#include + using namespace YACReader; bool InitialComicInfoExtractor::crash = false; InitialComicInfoExtractor::InitialComicInfoExtractor(QString fileSource, QString target, int coverPage, bool getXMLMetadata) - : _fileSource(fileSource), _target(target), _numPages(0), _coverPage(coverPage), getXMLMetadata(getXMLMetadata), _xmlInfoData() + : _fileSource(fileSource), _target(target), _numPages(0), _coverSize(0, 0), _coverExtracted(false), _coverPage(coverPage), getXMLMetadata(getXMLMetadata), _xmlInfoData() { if (coverPage <= 0) { _coverPage = 1; @@ -62,13 +65,16 @@ void InitialComicInfoExtractor::extract() #else QImage p = pdfComic->page(_coverPage - 1)->renderToImage(72, 72); #endif // - _cover = p; - _coverSize = QPair(p.width(), p.height()); - if (_target != "") { - saveCover(_target, p); - } else if (_target != "") { - QLOG_WARN() << "Extracting cover: requested cover index greater than numPages " << _fileSource; + if (!p.isNull()) { + _cover = p; + _coverSize = QPair(p.width(), p.height()); + _coverExtracted = true; + if (_target != "") { + saveCover(_target, p); + } } + } else { + QLOG_WARN() << "Extracting cover: requested cover index greater than numPages " << _fileSource; } return; } @@ -89,12 +95,14 @@ void InitialComicInfoExtractor::extract() } QList order = archive.getFileNames(); + const bool isEpub = Comic::fileIsEpub(_fileSource); if (getXMLMetadata) { // Try to find embeded XML info (ComicRack or ComicTagger) auto infoIndex = 0; - for (auto &fileName : order) { - if (fileName.endsWith(".xml", Qt::CaseInsensitive)) { + for (const QString &fileName : std::as_const(order)) { + const bool isComicInfo = QFileInfo(fileName).fileName().compare(QStringLiteral("ComicInfo.xml"), Qt::CaseInsensitive) == 0; + if (isComicInfo) { _xmlInfoData = archive.getRawDataAtIndex(infoIndex); break; } @@ -110,8 +118,26 @@ void InitialComicInfoExtractor::extract() } // se filtran para obtener sólo los formatos soportados - QList fileNames = FileComic::filter(order); - _numPages = fileNames.size(); + int coverArchiveIndex = -1; + if (isEpub) { + const auto epub = FileComic::epubScanInfo(order, archive, _coverPage); + if (!epub.isValid()) { + QLOG_WARN() << "Extracting cover: unsupported EPUB" << _fileSource << epub.error; + } + _numPages = epub.pageCount; + coverArchiveIndex = epub.coverArchiveIndex; + } else { + QList fileNames = FileComic::filter(order); + std::sort(fileNames.begin(), fileNames.end(), naturalSortLessThanCI); + _numPages = fileNames.size(); + if (_coverPage > _numPages) { + _coverPage = 1; + } + if (_numPages > 0) { + coverArchiveIndex = order.indexOf(fileNames.at(_coverPage - 1)); + } + } + if (_numPages == 0) { QLOG_WARN() << "Extracting cover: empty comic " << _fileSource; _cover.load(":/images/notCover.png"); @@ -119,21 +145,26 @@ void InitialComicInfoExtractor::extract() _cover.save(_target); } } else { - if (_coverPage > _numPages) { - _coverPage = 1; + if (coverArchiveIndex < 0) { + QLOG_WARN() << "Extracting cover: unable to resolve cover image " << _fileSource; + _cover.load(":/images/notCover.png"); + return; } - std::sort(fileNames.begin(), fileNames.end(), naturalSortLessThanCI); - int index = order.indexOf(fileNames.at(_coverPage - 1)); if (_target == "") { - if (!_cover.loadFromData(archive.getRawDataAtIndex(index))) { + if (_cover.loadFromData(archive.getRawDataAtIndex(coverArchiveIndex))) { + _coverSize = QPair(_cover.width(), _cover.height()); + _coverExtracted = true; + } else { QLOG_WARN() << "Extracting cover: unable to load image from extracted cover " << _fileSource; _cover.load(":/images/notCover.png"); } } else { QImage p; - if (p.loadFromData(archive.getRawDataAtIndex(index))) { + if (p.loadFromData(archive.getRawDataAtIndex(coverArchiveIndex))) { + _cover = p; _coverSize = QPair(p.width(), p.height()); + _coverExtracted = true; saveCover(_target, p); } else { QLOG_WARN() << "Extracting cover: unable to load image from extracted cover " << _fileSource; diff --git a/YACReaderLibrary/initial_comic_info_extractor.h b/YACReaderLibrary/initial_comic_info_extractor.h index 3e8f99cea..e02ca6cd5 100644 --- a/YACReaderLibrary/initial_comic_info_extractor.h +++ b/YACReaderLibrary/initial_comic_info_extractor.h @@ -22,6 +22,7 @@ class InitialComicInfoExtractor : public QObject int _numPages; QPair _coverSize; QImage _cover; + bool _coverExtracted; int _coverPage; int getXMLMetadata; static bool crash; @@ -32,6 +33,8 @@ public slots: void extract(); int getNumPages() { return _numPages; } QPixmap getCover() { return QPixmap::fromImage(_cover); } + QImage getCoverImage() const { return _cover; } + bool hasValidCover() const { return _coverExtracted; } QPair getOriginalCoverSize() { return _coverSize; } QByteArray getXMLInfoRawData(); signals: diff --git a/YACReaderLibrary/libraries_update_coordinator.cpp b/YACReaderLibrary/libraries_update_coordinator.cpp index 30de3d677..77aea39ab 100644 --- a/YACReaderLibrary/libraries_update_coordinator.cpp +++ b/YACReaderLibrary/libraries_update_coordinator.cpp @@ -2,6 +2,7 @@ #include "libraries_update_coordinator.h" #include "library_creator.h" +#include "xml_info_library_scanner.h" #include "yacreader_global.h" #include "yacreader_libraries.h" @@ -119,6 +120,24 @@ LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::requ return startUpdate({ path }); } +LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::requestSingleLibraryXmlRescan(int id) +{ + if (isRunning()) { + return UpdateRequestResult::AlreadyRunning; + } + + const QString path = libraries.getPath(id); + if (path.isEmpty()) { + return UpdateRequestResult::LibraryNotFound; + } + + if (!canStartUpdateProvider()) { + return UpdateRequestResult::NotAllowed; + } + + return startXmlRescan(path); +} + bool LibrariesUpdateCoordinator::isRunning() const { QMutexLocker locker(&futureMutex); @@ -155,6 +174,26 @@ LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::star return UpdateRequestResult::Started; } +LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::startXmlRescan(const QString &path) +{ + QMutexLocker locker(&futureMutex); + + if (updateFuture.valid() && updateFuture.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { + return UpdateRequestResult::AlreadyRunning; + } + + canceled = false; + updateFuture = std::async(std::launch::async, [this, path] { + emit updateStarted(); + if (!canceled) { + rescanLibraryXml(path); + } + emit updateEnded(); + }); + + return UpdateRequestResult::Started; +} + void LibrariesUpdateCoordinator::updateLibrary(const QString &path) { QDir pathDir(path); @@ -177,6 +216,25 @@ void LibrariesUpdateCoordinator::updateLibrary(const QString &path) eventLoop.exec(); } +void LibrariesUpdateCoordinator::rescanLibraryXml(const QString &path) +{ + QDir pathDir(path); + if (!pathDir.exists()) { + return; + } + + QEventLoop eventLoop; + auto scanner = new XMLInfoLibraryScanner(); + std::shared_ptr sharedPtr(scanner); + currentXmlInfoLibraryScanner = sharedPtr; + + const QString cleanPath = QDir::cleanPath(pathDir.absolutePath()); + connect(scanner, &XMLInfoLibraryScanner::finished, &eventLoop, &QEventLoop::quit); + + scanner->scanLibrary(cleanPath, LibraryPaths::libraryDataPath(cleanPath)); + eventLoop.exec(); +} + void LibrariesUpdateCoordinator::stop() { canceled = true; @@ -184,6 +242,10 @@ void LibrariesUpdateCoordinator::stop() if (auto libraryCreator = currentLibraryCreator.lock()) { libraryCreator->stop(); } + + if (auto scanner = currentXmlInfoLibraryScanner.lock()) { + scanner->stop(); + } } void LibrariesUpdateCoordinator::cancel() @@ -193,4 +255,8 @@ void LibrariesUpdateCoordinator::cancel() if (auto libraryCreator = currentLibraryCreator.lock()) { libraryCreator->cancel(); } + + if (auto scanner = currentXmlInfoLibraryScanner.lock()) { + scanner->stop(); + } } diff --git a/YACReaderLibrary/libraries_update_coordinator.h b/YACReaderLibrary/libraries_update_coordinator.h index 814e3a06b..e48d7a080 100644 --- a/YACReaderLibrary/libraries_update_coordinator.h +++ b/YACReaderLibrary/libraries_update_coordinator.h @@ -6,6 +6,9 @@ class YACReaderLibraries; class LibraryCreator; +namespace YACReader { +class XMLInfoLibraryScanner; +} class LibrariesUpdateCoordinator : public QObject { @@ -24,6 +27,7 @@ class LibrariesUpdateCoordinator : public QObject bool isRunning() const; UpdateRequestResult requestLibrariesUpdate(); UpdateRequestResult requestSingleLibraryUpdate(int id); + UpdateRequestResult requestSingleLibraryXmlRescan(int id); public slots: void updateLibraries(); @@ -40,7 +44,9 @@ private slots: private: UpdateRequestResult startUpdate(const QStringList &paths); + UpdateRequestResult startXmlRescan(const QString &path); void updateLibrary(const QString &path); + void rescanLibraryXml(const QString &path); QSettings *settings; YACReaderLibraries &libraries; @@ -50,6 +56,7 @@ private slots: mutable QMutex futureMutex; bool canceled; std::weak_ptr currentLibraryCreator; + std::weak_ptr currentXmlInfoLibraryScanner; std::function canStartUpdateProvider; }; diff --git a/YACReaderLibrary/library_creator.cpp b/YACReaderLibrary/library_creator.cpp index 3b55c3af3..ca578aced 100644 --- a/YACReaderLibrary/library_creator.cpp +++ b/YACReaderLibrary/library_creator.cpp @@ -46,7 +46,7 @@ Folder rootFolder(QSqlDatabase &db) LibraryCreator::LibraryCreator(QSettings *settings) : creation(false), partialUpdate(false), settings(settings) { - _nameFilter << Comic::comicExtensions; + _nameFilter = Comic::comicExtensions; } void LibraryCreator::createLibrary(const QString &source, const QString &target) @@ -57,6 +57,7 @@ void LibraryCreator::createLibrary(const QString &source, const QString &target) void LibraryCreator::updateLibrary(const QString &source, const QString &target) { + creation = false; checkModifiedDatesOnUpdate = settings->value(COMPARE_MODIFIED_DATE_ON_LIBRARY_UPDATES, false).toBool(); partialUpdate = false; _source = source; @@ -66,6 +67,7 @@ void LibraryCreator::updateLibrary(const QString &source, const QString &target) void LibraryCreator::updateFolder(const QString &source, const QString &target, const QString &sourceFolder, qulonglong folderId) { + creation = false; checkModifiedDatesOnUpdate = settings->value(COMPARE_MODIFIED_DATE_ON_LIBRARY_UPDATES, false).toBool(); partialUpdate = true; _folderDestinationId = folderId; @@ -124,10 +126,7 @@ void LibraryCreator::processLibrary(const QString &source, const QString &target _source = source; _target = target; if (DataBaseManagement::checkValidDB(target + "/library.ydb") == "") { - // se limpia el directorio ./yacreaderlibrary - QDir d(target); - d.removeRecursively(); - _mode = CREATOR; + _mode = creation ? CREATOR : UPDATER; } else { // _mode = UPDATER; } @@ -148,8 +147,37 @@ void LibraryCreator::run() } sevenzLib->deleteLater(); #endif + if (_mode == CREATOR) + QDir().mkpath(_target); + + LibraryMaintenanceLock maintenanceLock(_source); + if (!maintenanceLock.tryLock()) { + const auto error = maintenanceLock.errorString(); + QLOG_ERROR() << error; + if (_mode == CREATOR) + emit failedCreatingDB(error); + else + emit failedOpeningDB(error); + return; + } + + if (_mode == UPDATER) { + QString recoveryError; + if (!DataBaseManagement::recoverInterruptedRestore(_source, &recoveryError, true)) { + QLOG_ERROR() << recoveryError; + emit failedOpeningDB(recoveryError); + return; + } + } + if (_mode == CREATOR) { QLOG_INFO() << "Starting to create new library ( " << _source << "," << _target << ")"; + QString cleanupError; + if (!DataBaseManagement::prepareForRecreation(_source, &cleanupError, true)) { + QLOG_ERROR() << cleanupError; + emit failedCreatingDB(cleanupError); + return; + } _currentPathFolders.clear(); // se crean los directorios .yacreaderlibrary y .yacreaderlibrary/covers QDir dir; @@ -183,6 +211,13 @@ void LibraryCreator::run() QLOG_INFO() << "Create library END"; } else { QLOG_INFO() << "Starting to update folder" << _sourceFolder << "in library ( " << _source << "," << _target << ")"; + QString backupError; + if (!DataBaseManagement::backupLibrary(_source, DatabaseBackupReason::AutoUpdate, &backupError)) { + const auto error = QString("Unable to back up library database: %1").arg(backupError); + QLOG_ERROR() << error; + emit failedOpeningDB(error); + return; + } { auto _database = DataBaseManagement::loadDatabase(_target); diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp index d8af4810a..49f0ebf7e 100644 --- a/YACReaderLibrary/library_window.cpp +++ b/YACReaderLibrary/library_window.cpp @@ -41,6 +41,7 @@ #include "api_key_dialog.h" #include "comic_db.h" #include "comic_files_manager.h" +#include "comic_info_repairer.h" #include "comic_model.h" #include "comic_vine_dialog.h" #include "comics_remover.h" @@ -69,6 +70,7 @@ #include "reading_list_model.h" #include "recent_visibility_coordinator.h" #include "rename_library_dialog.h" +#include "search_syntax_dialog.h" #include "server_config_dialog.h" #include "shortcuts_manager.h" #include "static.h" @@ -214,6 +216,7 @@ void LibraryWindow::setupUI() libraryCreator = new LibraryCreator(settings); packageManager = new PackageManager(); xmlInfoLibraryScanner = new XMLInfoLibraryScanner(); + comicInfoRepairer = new ComicInfoRepairer(settings); historyController = new YACReaderHistoryController(this); @@ -498,6 +501,13 @@ void LibraryWindow::createToolBars() libraryToolBar->setSearchWidget(searchEdit); #endif + auto *searchMenu = createSearchMenu(); +#ifdef Y_MAC_UI + libraryToolBar->setSearchMenu(searchMenu); +#else + searchEdit->setSearchMenu(searchMenu); +#endif + editInfoToolBar->setIconSize(QSize(18, 18)); editInfoToolBar->addAction(actions.openComicAction); editInfoToolBar->addSeparator(); @@ -540,6 +550,94 @@ void LibraryWindow::createToolBars() contentViewsManager->comicsView->setToolBar(editInfoToolBar); } +QMenu *LibraryWindow::createSearchMenu() +{ + auto *menu = new QMenu(tr("Search filters"), this); + menu->setMinimumWidth(190); + + auto addFilter = [this, menu](const QString &label, const QString &query) { + auto *action = menu->addAction(label); + connect(action, &QAction::triggered, this, [this, query] { + applySearchQuery(query); + }); + }; + + addFilter(tr("Unread"), QStringLiteral("read:false")); + addFilter( + tr("In progress"), + QStringLiteral("hasBeenOpened:true AND read:false")); + addFilter(tr("Highly rated"), QStringLiteral("rating>=4")); + + auto *recentlyAdded = menu->addAction(tr("Recently added")); + connect(recentlyAdded, &QAction::triggered, this, [this] { + const int days = settings->value(NUM_DAYS_TO_CONSIDER_RECENT, 1).toInt(); + applySearchQuery(QStringLiteral("added>%1").arg(days)); + }); + + menu->addSeparator(); + auto *syntaxAction = menu->addAction(tr("Search syntax…")); + connect(syntaxAction, &QAction::triggered, this, &LibraryWindow::showSearchSyntax); + + return menu; +} + +void LibraryWindow::applySearchQuery(const QString &query) +{ +#ifdef Y_MAC_UI + libraryToolBar->setSearchText(query); + libraryToolBar->focusSearch(); +#else + searchEdit->setText(query); + searchEdit->setFocus(Qt::ShortcutFocusReason); +#endif +} + +void LibraryWindow::setSearchInputEnabled(bool enabled) +{ +#ifdef Y_MAC_UI + libraryToolBar->setSearchEnabled(enabled); +#else + searchEdit->setEnabled(enabled); +#endif +} + +void LibraryWindow::clearSearchInput(bool notify) +{ +#ifdef Y_MAC_UI + libraryToolBar->clearSearchText(notify); +#else + if (notify) + searchEdit->clear(); + else + searchEdit->clearText(); +#endif +} + +void LibraryWindow::focusSearchInput() +{ +#ifdef Y_MAC_UI + libraryToolBar->focusSearch(); +#else + searchEdit->setFocus(Qt::ShortcutFocusReason); +#endif +} + +QString LibraryWindow::searchText() const +{ +#ifdef Y_MAC_UI + return libraryToolBar->searchText(); +#else + return searchEdit->text(); +#endif +} + +void LibraryWindow::showSearchSyntax() +{ + auto *dialog = new SearchSyntaxDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->open(); +} + void LibraryWindow::createMenus() { foldersView->addAction(actions.addFolderAction); @@ -642,6 +740,11 @@ void LibraryWindow::createMenus() typeMenu->addAction(setYonkomaAction); selectedLibrary->addAction(actions.rescanLibraryForXMLInfoAction); + selectedLibrary->addAction(actions.repairLibraryAction); + YACReader::addSperator(selectedLibrary); + + selectedLibrary->addAction(actions.backupLibraryAction); + selectedLibrary->addAction(actions.restoreLibraryAction); YACReader::addSperator(selectedLibrary); selectedLibrary->addAction(actions.exportComicsInfoAction); @@ -652,6 +755,7 @@ void LibraryWindow::createMenus() selectedLibrary->addAction(actions.importLibraryAction); YACReader::addSperator(selectedLibrary); + selectedLibrary->addAction(actions.openLibraryFolderAction); selectedLibrary->addAction(actions.showLibraryInfo); // MacOSX app menus @@ -672,6 +776,11 @@ void LibraryWindow::createMenus() libraryMenu->addSeparator(); libraryMenu->addAction(actions.rescanLibraryForXMLInfoAction); + libraryMenu->addAction(actions.repairLibraryAction); + libraryMenu->addSeparator(); + + libraryMenu->addAction(actions.backupLibraryAction); + libraryMenu->addAction(actions.restoreLibraryAction); libraryMenu->addSeparator(); libraryMenu->addAction(actions.exportComicsInfoAction); @@ -684,6 +793,7 @@ void LibraryWindow::createMenus() libraryMenu->addSeparator(); + libraryMenu->addAction(actions.openLibraryFolderAction); libraryMenu->addAction(actions.showLibraryInfo); // folder @@ -733,7 +843,7 @@ void LibraryWindow::createConnections() optionsDialog, serverConfigDialog, recentVisibilityCoordinator); - QObject::connect(actions.focusSearchLineAction, &QAction::triggered, searchEdit, [this] { searchEdit->setFocus(Qt::ShortcutFocusReason); }); + connect(actions.focusSearchLineAction, &QAction::triggered, this, &LibraryWindow::focusSearchInput); // libraryCreator connections connect(createLibraryDialog, &CreateLibraryDialog::createLibrary, this, QOverload::of(&LibraryWindow::create)); @@ -749,15 +859,80 @@ void LibraryWindow::createConnections() connect(libraryCreator, &LibraryCreator::comicAdded, importWidget, &ImportWidget::newComic); // libraryCreator errors connect(libraryCreator, &LibraryCreator::failedCreatingDB, this, &LibraryWindow::manageCreatingError); - // connect(libraryCreator, SIGNAL(failedUpdatingDB(QString)), this, SLOT(manageUpdatingError(QString))); // TODO: implement failedUpdatingDB + connect(libraryCreator, &LibraryCreator::failedOpeningDB, this, [this](const QString &error) { + showRootWidget(); + const auto libraryName = selectedLibrary->currentText(); + const auto libraryPath = libraries.getPath(libraryName); + if (!libraryPath.isEmpty() && QFile::exists(LibraryPaths::libraryDatabasePath(libraryPath)) && !DataBaseManagement::isLibraryDatabaseValid(libraryPath)) { + offerDatabaseRecovery(libraryName); + return; + } + manageUpdatingError(error); + }); connect(xmlInfoLibraryScanner, &QThread::finished, this, &LibraryWindow::showRootWidget); connect(xmlInfoLibraryScanner, &QThread::finished, this, &LibraryWindow::reloadCurrentFolderComicsContent); connect(xmlInfoLibraryScanner, &XMLInfoLibraryScanner::comicScanned, importWidget, &ImportWidget::newComic); + connect(comicInfoRepairer, &QThread::finished, this, [this]() { + const auto summary = comicInfoRepairer->summary(); + showRootWidget(); + reloadCurrentLibrary(); + + if (summary.lockedByAnotherProcess) { + if (summary.lockHolderIsRunningLocally) { + QMessageBox::information(this, + actions.repairLibraryAction->text(), + tr("A repair of this library is already running (%1). Wait for it to finish.").arg(summary.lockHolderInfo)); + return; + } + + auto text = summary.lockHolderInfo.isEmpty() + ? tr("The library is locked by a repair that did not finish.") + : tr("The library is locked by a repair started by %1.").arg(summary.lockHolderInfo); + text += "\n\n"; + text += tr("If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue?"); + + const auto answer = QMessageBox::question(this, + actions.repairLibraryAction->text(), + text, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (answer == QMessageBox::Yes) { + startLibraryRepair(true); + } + return; + } + + if (summary.canceled || !summary.error.isEmpty()) { + return; + } + + QMessageBox messageBox(QMessageBox::Information, + actions.repairLibraryAction->text(), + tr("Repaired: %1\nFailed: %2\nMissing files: %3").arg(summary.repaired).arg(summary.failed).arg(summary.missingFiles), + QMessageBox::Ok, + this); + if (!summary.failedFilePaths.isEmpty()) { + messageBox.setDetailedText(summary.failedFilePaths.join('\n')); + } + messageBox.exec(); + }); + connect(comicInfoRepairer, &ComicInfoRepairer::comicProcessed, importWidget, &ImportWidget::newComic); + connect(comicInfoRepairer, &ComicInfoRepairer::failed, this, [this](const QString &error) { + const auto libraryName = selectedLibrary->currentText(); + const auto libraryPath = libraries.getPath(libraryName); + if (!libraryPath.isEmpty() && QFile::exists(LibraryPaths::libraryDatabasePath(libraryPath)) && !DataBaseManagement::isLibraryDatabaseValid(libraryPath)) { + offerDatabaseRecovery(libraryName); + return; + } + QMessageBox::critical(this, actions.repairLibraryAction->text(), error); + }); + // new import widget connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopLibraryCreator); connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopXMLScanning); + connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopComicInfoRepair); // packageManager connections connect(exportLibraryDialog, &ExportLibraryDialog::exportPath, this, &LibraryWindow::exportLibrary); @@ -769,6 +944,9 @@ void LibraryWindow::createConnections() connect(importLibraryDialog, &ImportLibraryDialog::libraryExists, this, &LibraryWindow::libraryAlreadyExists); connect(packageManager, &PackageManager::imported, importLibraryDialog, &QWidget::hide); connect(packageManager, &PackageManager::imported, this, &LibraryWindow::openLastCreated); + connect(packageManager, &PackageManager::failed, this, [this](const QString &error) { + QMessageBox::critical(this, tr("Package operation failed"), error.isEmpty() ? tr("The covers package operation could not be completed.") : error); + }); // create and update dialogs connect(createLibraryDialog, &CreateLibraryDialog::cancelCreate, this, &LibraryWindow::cancelCreating); @@ -853,6 +1031,11 @@ void LibraryWindow::loadLibrary(const QString &name) showRootWidget(); QString rootPath = libraries.getPath(name); + QString recoveryError; + if (!DataBaseManagement::recoverInterruptedRestore(rootPath, &recoveryError)) { + QMessageBox::critical(this, tr("Restore recovery failed"), recoveryError); + return; + } QString path = LibraryPaths::libraryDataPath(rootPath); QString customFolderCoversPath = LibraryPaths::libraryCustomFoldersCoverPath(rootPath); QString databasePath = LibraryPaths::libraryDatabasePath(rootPath); @@ -866,6 +1049,20 @@ void LibraryWindow::loadLibrary(const QString &name) int comparation = DataBaseManagement::compareVersions(dbVersion, DB_VERSION); if (comparation < 0) { + // a database that fails validation would block the upgrade backup and + // trap the user in the update-needed/upgrade-failed dialog cycle; + // offer recovery instead of the upgrade question + if (!DataBaseManagement::isLibraryDatabaseValid(rootPath)) { + contentViewsManager->comicsView->setModel(NULL); + foldersView->setModel(NULL); + listsView->setModel(NULL); + actions.disableAllActions(); + actions.renameLibraryAction->setEnabled(true); + actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); + offerDatabaseRecovery(name); + return; + } int ret = QMessageBox::question(this, tr("Update needed"), tr("This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now?"), QMessageBox::Yes, QMessageBox::No); if (ret == QMessageBox::Yes) { importWidget->setUpgradeLook(); @@ -889,6 +1086,7 @@ void LibraryWindow::loadLibrary(const QString &name) // será possible renombrar y borrar estas bibliotecas actions.renameLibraryAction->setEnabled(true); actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); } } @@ -914,6 +1112,7 @@ void LibraryWindow::loadLibrary(const QString &name) { actions.disableLibrariesActions(false); actions.updateLibraryAction->setDisabled(true); + actions.repairLibraryAction->setDisabled(true); actions.openContainingFolderAction->setDisabled(true); actions.rescanLibraryForXMLInfoAction->setDisabled(true); @@ -931,7 +1130,7 @@ void LibraryWindow::loadLibrary(const QString &name) setRootIndex(); - searchEdit->clear(); + clearSearchInput(true); } else if (comparation > 0) { int ret = QMessageBox::question(this, tr("Download new version"), tr("This library was created with a newer version of YACReaderLibrary. Download the new version now?"), QMessageBox::Yes, QMessageBox::No); if (ret == QMessageBox::Yes) @@ -944,6 +1143,7 @@ void LibraryWindow::loadLibrary(const QString &name) // será possible renombrar y borrar estas bibliotecas actions.renameLibraryAction->setEnabled(true); actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); } } else { contentViewsManager->comicsView->setModel(NULL); @@ -960,6 +1160,7 @@ void LibraryWindow::loadLibrary(const QString &name) // será possible renombrar y borrar estas bibliotecas actions.renameLibraryAction->setEnabled(true); actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); } else // si existe el path, puede ser que la librería sea alguna versión pre-5.0 ó que esté corrupta o que no haya drivers sql { @@ -970,17 +1171,17 @@ void LibraryWindow::loadLibrary(const QString &name) // será possible renombrar y borrar estas bibliotecas actions.renameLibraryAction->setEnabled(true); actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); } else { QString currentLibrary = selectedLibrary->currentText(); QString path = libraries.getPath(selectedLibrary->currentText()); if (QMessageBox::question(this, tr("Old library"), tr("Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now?").arg(currentLibrary), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) { - QDir d(LibraryPaths::libraryDataPath(path)); - d.removeRecursively(); createLibraryDialog->setDataAndStart(currentLibrary, path); } // será possible renombrar y borrar estas bibliotecas actions.renameLibraryAction->setEnabled(true); actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); } } } @@ -1879,12 +2080,256 @@ void LibraryWindow::updateLibrary() libraryCreator->start(); } +void LibraryWindow::backupLibrary() +{ + const auto path = libraries.getPath(selectedLibrary->currentText()); + if (path.isEmpty()) + return; + + auto version = DataBaseManagement::checkValidDB(LibraryPaths::libraryDatabasePath(path)); + if (version.isEmpty()) + version = "unknown"; + const auto suggestedName = QString("library-%1-db-%2-manual.ydb") + .arg(QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss"), version); + const auto destination = QFileDialog::getSaveFileName(this, + actions.backupLibraryAction->text(), + QDir::home().filePath(suggestedName), + tr("YACReader library database (*.ydb)")); + if (destination.isEmpty()) + return; + + struct BackupResult { + bool success { false }; + QString error; + }; + + auto result = std::make_shared(); + auto worker = QThread::create([path, destination, result] { + result->success = DataBaseManagement::backupLibrary(path, DatabaseBackupReason::Manual, &result->error, destination); + }); + + actions.backupLibraryAction->setDisabled(true); + connect(worker, &QThread::finished, this, [this, destination, result] { + actions.backupLibraryAction->setDisabled(false); + if (result->success) { + QMessageBox::information(this, + actions.backupLibraryAction->text(), + tr("The library database backup was created at:\n%1").arg(destination)); + } else { + QMessageBox::critical(this, + actions.backupLibraryAction->text(), + tr("Unable to create the library database backup:\n%1").arg(result->error)); + } + }); + connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); +} + +void LibraryWindow::restoreLibrary() +{ + const auto libraryPath = libraries.getPath(selectedLibrary->currentText()); + if (libraryPath.isEmpty()) + return; + + const auto backupPath = QFileDialog::getOpenFileName(this, + actions.restoreLibraryAction->text(), + QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("backups"), + tr("YACReader library database (*.ydb)")); + if (backupPath.isEmpty()) + return; + + const auto answer = QMessageBox::warning(this, + actions.restoreLibraryAction->text(), + tr("Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (answer == QMessageBox::Yes) + startLibraryRestore(backupPath); +} + +void LibraryWindow::startLibraryRestore(const QString &backupPath, bool allowInvalidCurrent, bool removeStaleLock) +{ + const auto libraryName = selectedLibrary->currentText(); + const auto libraryPath = libraries.getPath(libraryName); + auto result = std::make_shared(); + auto progress = new QProgressDialog(tr("Restoring library database..."), QString(), 0, 0, this); + progress->setCancelButton(nullptr); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + + contentViewsManager->comicsView->setModel(nullptr); + foldersView->setModel(nullptr); + listsView->setModel(nullptr); + actions.disableAllActions(); + + auto worker = QThread::create([libraryPath, backupPath, allowInvalidCurrent, removeStaleLock, result] { + *result = DataBaseManagement::restoreLibrary(libraryPath, backupPath, allowInvalidCurrent, removeStaleLock); + }); + connect(worker, &QThread::finished, this, [this, libraryName, backupPath, allowInvalidCurrent, result, progress] { + progress->deleteLater(); + + if (result->status == DatabaseRestoreStatus::InvalidCurrentDatabase && !allowInvalidCurrent) { + const auto answer = QMessageBox::warning(this, + actions.restoreLibraryAction->text(), + tr("The current library database is invalid. Restore the selected backup anyway?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (answer == QMessageBox::Yes) { + startLibraryRestore(backupPath, true); + return; + } + actions.renameLibraryAction->setEnabled(true); + actions.removeLibraryAction->setEnabled(true); + actions.restoreLibraryAction->setEnabled(true); + return; + } else if (result->status == DatabaseRestoreStatus::LockFailed && !result->lockHolderIsRunningLocally) { + const auto answer = QMessageBox::warning(this, + actions.restoreLibraryAction->text(), + tr("The library maintenance lock may be stale. Remove it and retry?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (answer == QMessageBox::Yes) { + startLibraryRestore(backupPath, allowInvalidCurrent, true); + return; + } + loadLibrary(libraryName); + return; + } + + if (!result->success()) { + auto error = result->error; + if (result->status == DatabaseRestoreStatus::RollbackFailed) + error += tr("\n\nRestart YACReaderLibrary before attempting recovery again."); + QMessageBox::critical(this, actions.restoreLibraryAction->text(), error); + if (result->status != DatabaseRestoreStatus::RollbackFailed) { + loadLibrary(libraryName); + } else { + actions.restoreLibraryAction->setEnabled(true); + actions.removeLibraryAction->setEnabled(true); + } + return; + } + + loadLibrary(libraryName); + const auto answer = QMessageBox::question(this, + actions.restoreLibraryAction->text(), + tr("The library database was restored successfully. Update the library now?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes); + if (answer == QMessageBox::Yes) + updateLibrary(); + }); + connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); +} + +void LibraryWindow::offerDatabaseRecovery(const QString &libraryName) +{ + QMessageBox messageBox(QMessageBox::Warning, + tr("Library database damaged"), + tr("The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed.").arg(libraryName), + QMessageBox::NoButton, + this); + const auto repairButton = messageBox.addButton(tr("Attempt repair"), QMessageBox::AcceptRole); + const auto restoreButton = messageBox.addButton(tr("Restore a backup..."), QMessageBox::ActionRole); + messageBox.addButton(QMessageBox::Cancel); + messageBox.setWindowModality(Qt::WindowModal); + messageBox.exec(); + + if (messageBox.clickedButton() == repairButton) + startDatabaseSalvage(libraryName); + else if (messageBox.clickedButton() == restoreButton) + restoreLibrary(); +} + +void LibraryWindow::startDatabaseSalvage(const QString &libraryName, bool removeStaleLock) +{ + const auto libraryPath = libraries.getPath(libraryName); + if (libraryPath.isEmpty()) + return; + + auto result = std::make_shared(); + auto progress = new QProgressDialog(tr("Repairing library database..."), QString(), 0, 0, this); + progress->setCancelButton(nullptr); + progress->setWindowModality(Qt::WindowModal); + progress->setMinimumDuration(0); + + auto worker = QThread::create([libraryPath, removeStaleLock, result] { + *result = DataBaseManagement::salvageLibrary(libraryPath, removeStaleLock); + }); + connect(worker, &QThread::finished, this, [this, libraryName, result, progress] { + progress->deleteLater(); + + if (result->status == DatabaseSalvageStatus::LockFailed) { + if (!result->lockHolderIsRunningLocally) { + const auto answer = QMessageBox::warning(this, + tr("Library database repair"), + tr("The library maintenance lock may be stale. Remove it and retry?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (answer == QMessageBox::Yes) + startDatabaseSalvage(libraryName, true); + } else { + QMessageBox::warning(this, + tr("Library database repair"), + tr("Another maintenance operation is currently using this library. Try again after it finishes.")); + } + return; + } + + if (result->success()) { + loadLibrary(libraryName); + if (result->status == DatabaseSalvageStatus::AlreadyValid) { + QMessageBox::information(this, + tr("Library database repair"), + tr("The library database is already valid.")); + } else if (result->status == DatabaseSalvageStatus::Reindexed) { + QMessageBox::information(this, + tr("Library database repaired"), + tr("The library database was repaired by rebuilding its indexes. The damaged original was preserved at:\n%1").arg(result->preservedDatabasePath)); + } else { + const auto answer = QMessageBox::question(this, + tr("Library database rebuilt"), + tr("The library database was rebuilt successfully. The damaged original was preserved at:\n%1\n\nUpdate the library now?").arg(result->preservedDatabasePath), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes); + if (answer == QMessageBox::Yes) + updateLibrary(); + } + } else { + auto recovery = result->preservedDatabasePath.isEmpty() + ? QString() + : tr("\n\nThe damaged original was preserved at:\n%1").arg(result->preservedDatabasePath); + QMessageBox::critical(this, + tr("Library database repair failed"), + tr("The library database could not be repaired:\n%1%2\n\nYou can restore a backup from the Library menu or recreate the library.").arg(result->error, recovery)); + actions.restoreLibraryAction->setEnabled(true); + } + }); + connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); +} + +void LibraryWindow::repairLibrary() +{ + startLibraryRepair(false); +} + +void LibraryWindow::startLibraryRepair(bool removeStaleLock) +{ + importWidget->setRepairLook(); + showImportingWidget(); + + const auto path = libraries.getPath(selectedLibrary->currentText()); + comicInfoRepairer->repairLibrary(path, LibraryPaths::libraryDataPath(path), removeStaleLock); +} + void LibraryWindow::deleteCurrentLibrary() { QString path = libraries.getPath(selectedLibrary->currentText()); libraries.remove(selectedLibrary->currentText()); selectedLibrary->removeItem(selectedLibrary->currentIndex()); - path = LibraryPaths::libraryDatabasePath(path); + path = LibraryPaths::libraryDataPath(path); QDir d(path); d.removeRecursively(); @@ -1908,7 +2353,7 @@ void LibraryWindow::removeLibrary() tr("Do you want remove ") + currentLibrary + tr(" library?"), QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No, this); - messageBox->button(QMessageBox::YesToAll)->setText(tr("Remove and delete metadata")); + messageBox->button(QMessageBox::YesToAll)->setText(tr("Remove and delete metadata and backups")); messageBox->setWindowModality(Qt::WindowModal); int ret = messageBox->exec(); if (ret == QMessageBox::Yes) { @@ -1987,6 +2432,13 @@ void LibraryWindow::showLibraryInfo() msgBox.exec(); } +void LibraryWindow::openLibraryFolder() +{ + const auto path = libraries.getPath(selectedLibrary->currentText()); + if (!path.isEmpty()) + QDesktopServices::openUrl(QUrl::fromLocalFile(QDir::cleanPath(path))); +} + void LibraryWindow::rescanCurrentFolderForXMLInfo() { rescanFolderForXMLInfo(getCurrentFolderIndex()); @@ -2021,6 +2473,12 @@ void LibraryWindow::stopXMLScanning() xmlInfoLibraryScanner->wait(); } +void LibraryWindow::stopComicInfoRepair() +{ + comicInfoRepairer->stop(); + comicInfoRepairer->wait(); +} + void LibraryWindow::setRootIndex() { if (!libraries.isEmpty()) { @@ -2236,17 +2694,12 @@ void LibraryWindow::openContainingFolderComic() #endif #ifdef Q_OS_MACOS - QString filePath = file.absoluteFilePath(); + // `open -R` reveals and selects the file in Finder without sending an Apple + // Event, so it doesn't trigger the macOS automation permission prompt. QStringList args; - args << "-e"; - args << "tell application \"Finder\""; - args << "-e"; - args << "activate"; - args << "-e"; - args << "select POSIX file \"" + filePath + "\""; - args << "-e"; - args << "end tell"; - QProcess::startDetached("osascript", args); + args << "-R"; + args << file.absoluteFilePath(); + QProcess::startDetached("open", args); #endif #ifdef Q_OS_WIN @@ -2390,7 +2843,7 @@ void LibraryWindow::showExportComicsInfo() void LibraryWindow::showImportComicsInfo() { - importComicsInfoDialog->dest = currentPath() + LibraryPaths::libraryDatabasePath(currentPath()); + importComicsInfoDialog->dest = LibraryPaths::libraryDatabasePath(currentPath()); importComicsInfoDialog->open(); } @@ -2408,6 +2861,7 @@ void LibraryWindow::prepareToCloseApp() libraryCreator->stop(); librariesUpdateCoordinator->stop(); + stopComicInfoRepair(); settings->setValue(MAIN_WINDOW_GEOMETRY, saveGeometry()); settings->setValue(MAIN_WINDOW_STATE, saveState()); @@ -2428,7 +2882,7 @@ void LibraryWindow::closeApp() void LibraryWindow::showNoLibrariesWidget() { actions.disableAllActions(); - searchEdit->setDisabled(true); + setSearchInputEnabled(false); mainWidget->setCurrentIndex(1); } @@ -2437,7 +2891,7 @@ void LibraryWindow::showRootWidget() #ifndef Y_MAC_UI libraryToolBar->setDisabled(false); #endif - searchEdit->setEnabled(true); + setSearchInputEnabled(true); mainWidget->setCurrentIndex(0); } @@ -2448,7 +2902,7 @@ void LibraryWindow::showImportingWidget() #ifndef Y_MAC_UI libraryToolBar->setDisabled(true); #endif - searchEdit->setDisabled(true); + setSearchInputEnabled(false); mainWidget->setCurrentIndex(2); } @@ -2703,7 +3157,7 @@ bool LibraryWindow::exitSearchMode() { if (status != LibraryWindow::Searching) return false; - searchEdit->clearText(); + clearSearchInput(false); clearSearchFilter(); return true; } diff --git a/YACReaderLibrary/library_window.h b/YACReaderLibrary/library_window.h index af31dc430..c418d745d 100644 --- a/YACReaderLibrary/library_window.h +++ b/YACReaderLibrary/library_window.h @@ -28,6 +28,7 @@ class QTreeView; class QDirModel; class QAction; +class QMenu; class QToolBar; class QComboBox; class QThread; @@ -87,6 +88,7 @@ class RecentVisibilityCoordinator; namespace YACReader { class TrayIconController; class XMLInfoLibraryScanner; +class ComicInfoRepairer; } #include "comic_db.h" @@ -110,6 +112,7 @@ class LibraryWindow : public QMainWindow, protected Themable AddLibraryDialog *addLibraryDialog; LibraryCreator *libraryCreator; XMLInfoLibraryScanner *xmlInfoLibraryScanner; + ComicInfoRepairer *comicInfoRepairer; HelpAboutDialog *had; RenameLibraryDialog *renameLibraryDialog; PropertiesDialog *propertiesDialog; @@ -197,6 +200,12 @@ class LibraryWindow : public QMainWindow, protected Themable void doModels(); void setupCoordinators(); bool hasLoadedLibraryModels() const; + QMenu *createSearchMenu(); + void applySearchQuery(const QString &query); + void setSearchInputEnabled(bool enabled); + void clearSearchInput(bool notify); + void focusSearchInput(); + void showSearchSyntax(); QString currentPath(); QString currentFolderPath(); @@ -220,6 +229,7 @@ class LibraryWindow : public QMainWindow, protected Themable public: LibraryWindow(); + QString searchText() const; signals: void libraryUpgraded(const QString &libraryName); @@ -239,6 +249,13 @@ public slots: void reloadCurrentLibrary(); void openLastCreated(); void updateLibrary(); + void backupLibrary(); + void restoreLibrary(); + void startLibraryRestore(const QString &backupPath, bool allowInvalidCurrent = false, bool removeStaleLock = false); + void offerDatabaseRecovery(const QString &libraryName); + void startDatabaseSalvage(const QString &libraryName, bool removeStaleLock = false); + void repairLibrary(); + void startLibraryRepair(bool removeStaleLock); // void deleteLibrary(); void openContainingFolder(); void setFolderAsNotCompleted(); @@ -256,12 +273,14 @@ public slots: void renameLibrary(); void rescanLibraryForXMLInfo(); void showLibraryInfo(); + void openLibraryFolder(); void rescanCurrentFolderForXMLInfo(); void rescanFolderForXMLInfo(QModelIndex modelIndex); void rename(QString newName); void cancelCreating(); void stopLibraryCreator(); void stopXMLScanning(); + void stopComicInfoRepair(); void setRootIndex(); void toggleFullScreen(); void toNormal(); diff --git a/YACReaderLibrary/library_window_actions.cpp b/YACReaderLibrary/library_window_actions.cpp index 76957dcbd..d6b634d20 100644 --- a/YACReaderLibrary/library_window_actions.cpp +++ b/YACReaderLibrary/library_window_actions.cpp @@ -68,6 +68,21 @@ void LibraryWindowActions::createActions(LibraryWindow *window, QSettings *setti updateLibraryAction->setData(UPDATE_LIBRARY_ACTION_YL); updateLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(UPDATE_LIBRARY_ACTION_YL)); + backupLibraryAction = new QAction(tr("Back up library database"), window); + backupLibraryAction->setToolTip(tr("Create a backup of the current library database")); + backupLibraryAction->setData(BACKUP_LIBRARY_ACTION_YL); + backupLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(BACKUP_LIBRARY_ACTION_YL)); + + restoreLibraryAction = new QAction(tr("Restore library database backup"), window); + restoreLibraryAction->setToolTip(tr("Restore the current library database from a backup")); + restoreLibraryAction->setData(RESTORE_LIBRARY_ACTION_YL); + restoreLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(RESTORE_LIBRARY_ACTION_YL)); + + repairLibraryAction = new QAction(tr("Repair covers and comic info"), window); + repairLibraryAction->setToolTip(tr("Retry comics with missing covers or incomplete information")); + repairLibraryAction->setData(REPAIR_LIBRARY_ACTION_YL); + repairLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(REPAIR_LIBRARY_ACTION_YL)); + renameLibraryAction = new QAction(tr("Rename library"), window); renameLibraryAction->setToolTip(tr("Rename current library")); renameLibraryAction->setData(RENAME_LIBRARY_ACTION_YL); @@ -83,6 +98,11 @@ void LibraryWindowActions::createActions(LibraryWindow *window, QSettings *setti rescanLibraryForXMLInfoAction->setData(RESCAN_LIBRARY_XML_INFO_ACTION_YL); rescanLibraryForXMLInfoAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(RESCAN_LIBRARY_XML_INFO_ACTION_YL)); + openLibraryFolderAction = new QAction(tr("Open library folder..."), window); + openLibraryFolderAction->setToolTip(tr("Open the root folder of the current library")); + openLibraryFolderAction->setData(OPEN_LIBRARY_FOLDER_ACTION_YL); + openLibraryFolderAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(OPEN_LIBRARY_FOLDER_ACTION_YL)); + showLibraryInfo = new QAction(tr("Show library info"), window); showLibraryInfo->setToolTip(tr("Show information about the current library")); showLibraryInfo->setData(SHOW_LIBRARY_INFO_ACTION_YL); @@ -511,10 +531,14 @@ void LibraryWindowActions::createConnections( QObject::connect(renameListAction, &QAction::triggered, window, &LibraryWindow::showRenameCurrentList); QObject::connect(updateLibraryAction, &QAction::triggered, window, &LibraryWindow::updateLibrary); + QObject::connect(backupLibraryAction, &QAction::triggered, window, &LibraryWindow::backupLibrary); + QObject::connect(restoreLibraryAction, &QAction::triggered, window, &LibraryWindow::restoreLibrary); + QObject::connect(repairLibraryAction, &QAction::triggered, window, &LibraryWindow::repairLibrary); QObject::connect(renameLibraryAction, &QAction::triggered, window, &LibraryWindow::renameLibrary); // connect(deleteLibraryAction,SIGNAL(triggered()),window,SLOT(deleteLibrary())); QObject::connect(removeLibraryAction, &QAction::triggered, window, &LibraryWindow::removeLibrary); QObject::connect(rescanLibraryForXMLInfoAction, &QAction::triggered, window, &LibraryWindow::rescanLibraryForXMLInfo); + QObject::connect(openLibraryFolderAction, &QAction::triggered, window, &LibraryWindow::openLibraryFolder); QObject::connect(showLibraryInfo, &QAction::triggered, window, &LibraryWindow::showLibraryInfo); QObject::connect(openComicAction, &QAction::triggered, window, QOverload<>::of(&LibraryWindow::openComic)); @@ -629,9 +653,13 @@ void LibraryWindowActions::setUpShortcutsManagement(EditShortcutsDialog *editSho << exportLibraryAction << importLibraryAction << updateLibraryAction + << backupLibraryAction + << restoreLibraryAction + << repairLibraryAction << renameLibraryAction << removeLibraryAction << rescanLibraryForXMLInfoAction + << openLibraryFolderAction << showLibraryInfo); allActions << tmpList; @@ -684,6 +712,9 @@ void LibraryWindowActions::disableComicsActions(bool disabled) void LibraryWindowActions::disableLibrariesActions(bool disabled) { updateLibraryAction->setDisabled(disabled); + backupLibraryAction->setDisabled(disabled); + restoreLibraryAction->setDisabled(disabled); + repairLibraryAction->setDisabled(disabled); renameLibraryAction->setDisabled(disabled); removeLibraryAction->setDisabled(disabled); exportComicsInfoAction->setDisabled(disabled); @@ -696,6 +727,9 @@ void LibraryWindowActions::disableLibrariesActions(bool disabled) void LibraryWindowActions::disableNoUpdatedLibrariesActions(bool disabled) { updateLibraryAction->setDisabled(disabled); + backupLibraryAction->setDisabled(disabled); + restoreLibraryAction->setDisabled(disabled); + repairLibraryAction->setDisabled(disabled); exportComicsInfoAction->setDisabled(disabled); importComicsInfoAction->setDisabled(disabled); exportLibraryAction->setDisabled(disabled); @@ -777,6 +811,7 @@ void LibraryWindowActions::updateTheme(const Theme &theme) updateLibraryAction->setIcon(menuIcons.updateLibraryIcon); renameLibraryAction->setIcon(menuIcons.renameLibraryIcon); removeLibraryAction->setIcon(menuIcons.removeLibraryIcon); + openLibraryFolderAction->setIcon(menuIcons.openContainingFolderIcon); openContainingFolderAction->setIcon(menuIcons.openContainingFolderIcon); openContainingFolderComicAction->setIcon(menuIcons.openContainingFolderIcon); updateFolderAction->setIcon(menuIcons.updateCurrentFolderIcon); diff --git a/YACReaderLibrary/library_window_actions.h b/YACReaderLibrary/library_window_actions.h index 0cffabdbb..4c60580ff 100644 --- a/YACReaderLibrary/library_window_actions.h +++ b/YACReaderLibrary/library_window_actions.h @@ -37,10 +37,14 @@ class LibraryWindowActions QAction *rescanLibraryForXMLInfoAction; QAction *updateLibraryAction; + QAction *backupLibraryAction; + QAction *restoreLibraryAction; + QAction *repairLibraryAction; QAction *removeLibraryAction; QAction *helpAboutAction; QAction *renameLibraryAction; + QAction *openLibraryFolderAction; QAction *showLibraryInfo; #ifndef Q_OS_MACOS diff --git a/YACReaderLibrary/options_dialog.cpp b/YACReaderLibrary/options_dialog.cpp index ff37d8859..b3c68b757 100644 --- a/YACReaderLibrary/options_dialog.cpp +++ b/YACReaderLibrary/options_dialog.cpp @@ -7,6 +7,7 @@ #include "theme_manager.h" #include "yacreader_3d_flow_config_widget.h" #include "yacreader_global_gui.h" +#include "yacreader_settings_widget.h" #include #include @@ -14,7 +15,6 @@ #include #include #include -#include #include FlowType flowType = Strip; @@ -29,12 +29,13 @@ OptionsDialog::OptionsDialog(QWidget *parent) auto appearanceW = createAppearanceTab(); - auto tabWidget = new QTabWidget(); - tabWidget->addTab(generalW, tr("General")); - tabWidget->addTab(librariesW, tr("Libraries")); - tabWidget->addTab(comicFlowW, tr("Comic Flow")); - tabWidget->addTab(gridViewW, tr("Grid view")); - tabWidget->addTab(appearanceW, tr("Appearance")); + auto settingsWidget = new YACReaderSettingsWidget(); + settingsWidget->addPage(generalW, tr("General")); + settingsWidget->addPage(librariesW, tr("Libraries")); + settingsWidget->addPage(comicFlowW, tr("Comic Flow")); + settingsWidget->addPage(gridViewW, tr("Grid view")); + settingsWidget->addPage(appearanceW, tr("Appearance")); + settingsWidget->addPage(shortcutsPage, shortcutsPage->windowTitle()); auto buttons = new QHBoxLayout(); buttons->addStretch(); @@ -43,13 +44,11 @@ OptionsDialog::OptionsDialog(QWidget *parent) buttons->addWidget(cancel); auto layout = new QVBoxLayout(this); - layout->addWidget(tabWidget); + layout->addWidget(settingsWidget); layout->addLayout(buttons); setLayout(layout); setModal(true); setWindowTitle(tr("Options")); - - this->layout()->setSizeConstraint(QLayout::SetFixedSize); } void OptionsDialog::editApiKey() @@ -253,7 +252,6 @@ QWidget *OptionsDialog::createGeneralTab() auto generalLayout = new QVBoxLayout(); generalLayout->addWidget(languageBox); generalLayout->addWidget(trayIconBox); - generalLayout->addWidget(shortcutsBox); generalLayout->addWidget(apiKeyBox); generalLayout->addWidget(comicInfoXMLBox); generalLayout->addWidget(recentlyAddedBox); @@ -332,10 +330,14 @@ QWidget *OptionsDialog::createLibrariesTab() librariesBoxLayout->addWidget(updateLibrariesAtCertainTimeCheck); librariesBoxLayout->addLayout(updateLibrariesAtCertainTimeLayout); - librariesBoxLayout->addWidget(new QLabel(tr("WARNING! During library updates writes to the database are disabled!\n" - "Don't schedule updates while you may be using the app actively.\n" - "During automatic updates the app will block some of the actions until the update is finished.\n" - "To stop an automatic update tap on the loading indicator next to the Libraries title."))); + // Without word wrapping this label is the widest widget in the whole dialog, and because + // QStackedWidget hints at the width of its widest page it would size every other section too. + auto updatesWarningLabel = new QLabel(tr("WARNING! During library updates writes to the database are disabled!\n" + "Don't schedule updates while you may be using the app actively.\n" + "During automatic updates the app will block some of the actions until the update is finished.\n" + "To stop an automatic update tap on the loading indicator next to the Libraries title.")); + updatesWarningLabel->setWordWrap(true); + librariesBoxLayout->addWidget(updatesWarningLabel); auto librariesBox = new QGroupBox(tr("Libraries")); librariesBox->setLayout(librariesBoxLayout); diff --git a/YACReaderLibrary/package_manager.cpp b/YACReaderLibrary/package_manager.cpp index 2c8278d84..5f05f3b46 100644 --- a/YACReaderLibrary/package_manager.cpp +++ b/YACReaderLibrary/package_manager.cpp @@ -3,41 +3,44 @@ #include PackageManager::PackageManager() - : _7z(nullptr) + : creating(false), _7z(nullptr) { } void PackageManager::createPackage(const QString &libraryPath, const QString &dest) { + creating = true; QStringList attributes; attributes << "a" << "-y" << "-ttar" << dest + ".clc" << libraryPath; - _7z = new QProcess(); - // TODO: Missing slot for openingError!!! - connect(_7z, SIGNAL(error(QProcess::ProcessError)), this, SLOT(openingError(QProcess::ProcessError))); - connect(_7z, QOverload::of(&QProcess::finished), this, &PackageManager::exported); -#if defined Q_OS_UNIX && !defined Q_OS_MACOS - _7z->start("7z", attributes); // TODO: use 7z.so -#else - _7z->start(QCoreApplication::applicationDirPath() + "/utils/7zip", attributes); // TODO: use 7z.dll -#endif + start7z(attributes); } void PackageManager::extractPackage(const QString &packagePath, const QString &destDir) { + creating = false; QStringList attributes; QString output = "-o"; output += destDir; attributes << "x" << "-y" << output << packagePath; - _7z = new QProcess(); - connect(_7z, SIGNAL(error(QProcess::ProcessError)), this, SLOT(openingError(QProcess::ProcessError))); - connect(_7z, SIGNAL(finished(int, QProcess::ExitStatus)), this, SIGNAL(imported())); + start7z(attributes); +} + +void PackageManager::start7z(const QStringList &arguments) +{ + if (_7z != nullptr) { + _7z->deleteLater(); + } + + _7z = new QProcess(this); + connect(_7z, &QProcess::errorOccurred, this, &PackageManager::handleError); + connect(_7z, QOverload::of(&QProcess::finished), this, &PackageManager::handleFinished); #if defined Q_OS_UNIX && !defined Q_OS_MACOS - _7z->start("7z", attributes); // TODO: use 7z.so + _7z->start("7z", arguments); // TODO: use 7z.so #else - _7z->start(QCoreApplication::applicationDirPath() + "/utils/7zip", attributes); // TODO: use 7z.dll + _7z->start(QCoreApplication::applicationDirPath() + "/utils/7zip", arguments); // TODO: use 7z.dll #endif } @@ -53,3 +56,23 @@ void PackageManager::cancel() } } } + +void PackageManager::handleFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + if (exitStatus == QProcess::NormalExit && exitCode == 0) { + if (creating) { + emit exported(); + } else { + emit imported(); + } + return; + } + + emit failed(_7z != nullptr ? QString::fromLocal8Bit(_7z->readAllStandardError()).trimmed() : QString()); +} + +void PackageManager::handleError(QProcess::ProcessError error) +{ + Q_UNUSED(error) + emit failed(_7z != nullptr ? _7z->errorString() : QString()); +} diff --git a/YACReaderLibrary/package_manager.h b/YACReaderLibrary/package_manager.h index 5e4f534b5..f5698a5fb 100644 --- a/YACReaderLibrary/package_manager.h +++ b/YACReaderLibrary/package_manager.h @@ -14,12 +14,17 @@ public slots: void cancel(); private: + void start7z(const QStringList &arguments); + void handleFinished(int exitCode, QProcess::ExitStatus exitStatus); + void handleError(QProcess::ProcessError error); + bool creating; QProcess *_7z; signals: void exported(); void imported(); + void failed(const QString &error); }; #endif diff --git a/YACReaderLibrary/search_syntax_dialog.cpp b/YACReaderLibrary/search_syntax_dialog.cpp new file mode 100644 index 000000000..f90e9e2c6 --- /dev/null +++ b/YACReaderLibrary/search_syntax_dialog.cpp @@ -0,0 +1,393 @@ +#include "search_syntax_dialog.h" + +#include "search_field_registry.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +QLabel *codeLabel(const QString &text, QWidget *parent = nullptr) +{ + auto *label = new QLabel(text, parent); + label->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + label->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); + label->setMargin(6); + label->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); + return label; +} + +QWidget *guideStep( + const QString &title, + const QString &description, + const QString &example, + QWidget *parent) +{ + auto *group = new QGroupBox(title, parent); + group->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *layout = new QVBoxLayout(group); + auto *descriptionLabel = new QLabel(description, group); + descriptionLabel->setWordWrap(true); + layout->addWidget(descriptionLabel); + layout->addWidget(codeLabel(example, group)); + return group; +} + +void configureReferenceLayout(QFormLayout *layout) +{ + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + layout->setRowWrapPolicy(QFormLayout::WrapLongRows); + layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); + layout->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop); +} + +void addFormRow(QFormLayout *layout, const QString &syntax, const QString &description) +{ + auto *syntaxLabel = new QLabel(syntax); + syntaxLabel->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + syntaxLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); + + auto *descriptionLabel = new QLabel(description); + descriptionLabel->setWordWrap(true); + layout->addRow(syntaxLabel, descriptionLabel); +} + +QString categoryName(SearchFieldCategory category) +{ + switch (category) { + case SearchFieldCategory::Common: + return SearchSyntaxDialog::tr("Common"); + case SearchFieldCategory::Credits: + return SearchSyntaxDialog::tr("Credits"); + case SearchFieldCategory::Story: + return SearchSyntaxDialog::tr("Story"); + case SearchFieldCategory::Publication: + return SearchSyntaxDialog::tr("Publication"); + case SearchFieldCategory::ReadingAndFiles: + return SearchSyntaxDialog::tr("Reading & files"); + case SearchFieldCategory::Folders: + return SearchSyntaxDialog::tr("Folders"); + } + + return { }; +} + +QList fieldCategories() +{ + return { + SearchFieldCategory::Common, + SearchFieldCategory::Credits, + SearchFieldCategory::Story, + SearchFieldCategory::Publication, + SearchFieldCategory::ReadingAndFiles, + SearchFieldCategory::Folders + }; +} + +QList fieldRow(const SearchFieldDefinition &field) +{ + auto item = [](const QString &text) { + auto *standardItem = new QStandardItem(text); + standardItem->setEditable(false); + standardItem->setToolTip(text); + return standardItem; + }; + + auto *key = item(field.key); + key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + auto *example = item(field.example); + example->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + return { + key, + item(field.description), + item(field.input), + example + }; +} + +void addExample( + QTreeWidgetItem *category, + const QString &query, + const QString &description) +{ + auto *item = new QTreeWidgetItem(category, { query, description }); + item->setFont(0, QFontDatabase::systemFont(QFontDatabase::FixedFont)); + item->setData(0, Qt::UserRole, query); +} + +} + +SearchSyntaxDialog::SearchSyntaxDialog(QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(tr("Search syntax")); + setWindowModality(Qt::WindowModal); + setMinimumSize(760, 480); + resize(900, 560); + + auto *subtitle = new QLabel( + tr("Search every comic and folder field, or build precise queries."), + this); + subtitle->setWordWrap(true); + + auto *tabs = new QTabWidget(this); + tabs->addTab(createQuickGuideTab(), tr("Quick guide")); + tabs->addTab( + createFieldsTab(), + tr("Fields (%1)").arg(searchFieldDefinitions().size())); + tabs->addTab(createExamplesTab(), tr("Examples")); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close); + + auto *layout = new QVBoxLayout(this); + layout->addWidget(subtitle); + layout->addWidget(tabs, 1); + layout->addWidget(buttons); +} + +QWidget *SearchSyntaxDialog::createQuickGuideTab() +{ + auto *tab = new QWidget(this); + auto *layout = new QVBoxLayout(tab); + + auto *plainSearch = new QGroupBox(tr("Start with a simple search"), tab); + plainSearch->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + auto *plainSearchLayout = new QVBoxLayout(plainSearch); + auto *plainSearchDescription = new QLabel( + tr("Just start typing. Plain text search across all metadata."), + plainSearch); + plainSearchDescription->setWordWrap(true); + plainSearchLayout->addWidget(plainSearchDescription); + plainSearchLayout->addWidget(codeLabel(QStringLiteral("san -> searchs `san` in any field of the database"), plainSearch)); + layout->addWidget(plainSearch); + + auto *steps = new QHBoxLayout(); + steps->addWidget(guideStep( + tr("1. Search everywhere"), + tr("Type any text or quoted text."), + QStringLiteral("\"hidden kingdom\""), + tab), + 1, + Qt::AlignTop); + steps->addWidget(guideStep( + tr("2. Target a field"), + tr("Use a field name followed by : or ="), + QStringLiteral("writer:Smith"), + tab), + 1, + Qt::AlignTop); + steps->addWidget(guideStep( + tr("3. Combine conditions"), + tr("Use AND, OR, NOT and parentheses."), + QStringLiteral("read:false AND rating>=4"), + tab), + 1, + Qt::AlignTop); + layout->addLayout(steps); + + auto *reference = new QHBoxLayout(); + + auto *operators = new QGroupBox(tr("Operators"), tab); + operators->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + auto *operatorsLayout = new QFormLayout(operators); + configureReferenceLayout(operatorsLayout); + addFormRow(operatorsLayout, tr(": or ="), tr("contains the text")); + addFormRow(operatorsLayout, QStringLiteral("=="), tr("matches the complete value")); + addFormRow(operatorsLayout, QStringLiteral("> >="), tr("greater than / at least")); + addFormRow(operatorsLayout, QStringLiteral("< <="), tr("less than / at most")); + addFormRow(operatorsLayout, tr("\"quoted text\""), tr("keeps spaces inside one value")); + reference->addWidget(operators, 1); + + auto *dates = new QGroupBox(tr("Dates and grouping"), tab); + dates->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + auto *datesLayout = new QFormLayout(dates); + configureReferenceLayout(datesLayout); + addFormRow(datesLayout, QStringLiteral("added>7"), tr("added in the last 7 days")); + addFormRow(datesLayout, QStringLiteral("added<30"), tr("added more than 30 days ago")); + addFormRow( + datesLayout, + QStringLiteral("(writer:Smith OR writer:Jones)"), + tr("group alternatives")); + reference->addWidget(dates, 1); + + layout->addLayout(reference); + + auto *tip = new QLabel( + tr("Tips:\nSpaces act like AND, and searches are not case-sensitive.\nUse quotes to include spaces in a value."), + tab); + tip->setWordWrap(true); + layout->addWidget(tip); + layout->addStretch(); + + return tab; +} + +QWidget *SearchSyntaxDialog::createFieldsTab() +{ + auto *tab = new QWidget(this); + auto *layout = new QVBoxLayout(tab); + + auto *filterEdit = new QLineEdit(tab); + filterEdit->setClearButtonEnabled(true); + filterEdit->setPlaceholderText(tr("Find a field…")); + layout->addWidget(filterEdit); + + auto *help = new QLabel( + tr("Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. " + "For date fields, the integer is a number of days (added>7 means added within the last 7 days)."), + tab); + help->setTextFormat(Qt::PlainText); + help->setWordWrap(true); + layout->addWidget(help); + + auto *sourceModel = new QStandardItemModel(tab); + sourceModel->setHorizontalHeaderLabels({ tr("Field"), + tr("Description"), + tr("Input"), + tr("Example") }); + + for (const auto category : fieldCategories()) { + QList categoryRow { + new QStandardItem(categoryName(category)), + new QStandardItem(), + new QStandardItem(), + new QStandardItem() + }; + for (auto *item : categoryRow) + item->setEditable(false); + + auto *categoryItem = categoryRow.first(); + QFont categoryFont = categoryItem->font(); + categoryFont.setBold(true); + categoryItem->setFont(categoryFont); + sourceModel->appendRow(categoryRow); + + for (const auto &field : searchFieldDefinitions()) { + if (field.category == category) + categoryItem->appendRow(fieldRow(field)); + } + } + + auto *proxyModel = new QSortFilterProxyModel(tab); + proxyModel->setSourceModel(sourceModel); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterKeyColumn(-1); + proxyModel->setRecursiveFilteringEnabled(true); + + auto *fieldsView = new QTreeView(tab); + fieldsView->setModel(proxyModel); + fieldsView->setAlternatingRowColors(true); + fieldsView->setEditTriggers(QAbstractItemView::NoEditTriggers); + fieldsView->setSelectionBehavior(QAbstractItemView::SelectRows); + fieldsView->setUniformRowHeights(true); + fieldsView->setTextElideMode(Qt::ElideRight); + auto *header = fieldsView->header(); + header->setStretchLastSection(false); + header->setMinimumSectionSize(80); + header->setSectionResizeMode(QHeaderView::Interactive); + header->resizeSection(0, 165); + header->resizeSection(1, 205); + header->resizeSection(2, 250); + header->resizeSection(3, 225); + fieldsView->expandAll(); + layout->addWidget(fieldsView, 1); + + connect(filterEdit, &QLineEdit::textChanged, proxyModel, &QSortFilterProxyModel::setFilterFixedString); + connect(filterEdit, &QLineEdit::textChanged, fieldsView, [fieldsView] { + fieldsView->expandAll(); + }); + + return tab; +} + +QWidget *SearchSyntaxDialog::createExamplesTab() +{ + auto *tab = new QWidget(this); + auto *layout = new QVBoxLayout(tab); + + auto *intro = new QLabel( + tr("Examples show the pattern—replace the values with your own."), + tab); + layout->addWidget(intro); + + auto *examples = new QTreeWidget(tab); + examples->setColumnCount(2); + examples->setHeaderLabels({ tr("Query"), tr("What it finds") }); + examples->setAlternatingRowColors(true); + examples->setSelectionBehavior(QAbstractItemView::SelectRows); + examples->setContextMenuPolicy(Qt::CustomContextMenu); + + auto *common = new QTreeWidgetItem(examples, { tr("Common filters") }); + addExample(common, QStringLiteral("read:false"), tr("Unread comics")); + addExample(common, QStringLiteral("hasBeenOpened:true AND read:false"), tr("Comics in progress")); + addExample(common, QStringLiteral("rating>=4"), tr("Highly rated comics")); + addExample(common, QStringLiteral("added>7"), tr("Comics added in the last 7 days")); + + auto *metadata = new QTreeWidgetItem(examples, { tr("Metadata") }); + addExample(metadata, QStringLiteral("series:\"Starfall Chronicles\""), tr("Search by series")); + addExample(metadata, QStringLiteral("writer:Smith"), tr("Search by writer")); + addExample(metadata, QStringLiteral("type:manga"), tr("Manga comics")); + addExample(metadata, QStringLiteral("tags:\"to review\""), tr("Search textual tags")); + + auto *advanced = new QTreeWidgetItem(examples, { tr("Advanced combinations") }); + addExample( + advanced, + QStringLiteral("writer:Smith OR writer:Jones"), + tr("Match either writer")); + addExample( + advanced, + QStringLiteral("(publisher:ExamplePress OR publisher:StoryHouse) read:false"), + tr("Group alternatives")); + addExample(advanced, QStringLiteral("NOT format:annual"), tr("Exclude a value")); + addExample( + advanced, + QStringLiteral("added<30 AND rating>=4"), + tr("Older, highly rated comics")); + + examples->expandAll(); + examples->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + examples->header()->setSectionResizeMode(1, QHeaderView::Stretch); + layout->addWidget(examples, 1); + + connect(examples, &QTreeWidget::customContextMenuRequested, examples, [examples, this](const QPoint &position) { + auto *item = examples->itemAt(position); + if (!item) + return; + + const QString query = item->data(0, Qt::UserRole).toString(); + if (query.isEmpty()) + return; + + QMenu menu(examples); + auto *copyAction = menu.addAction(tr("Copy query")); + if (menu.exec(examples->viewport()->mapToGlobal(position)) == copyAction) + QApplication::clipboard()->setText(query); + }); + + auto *note = new QLabel( + tr("Spaces behave like AND. Use quotes for phrases and parentheses to control grouping."), + tab); + note->setWordWrap(true); + layout->addWidget(note); + + return tab; +} diff --git a/YACReaderLibrary/search_syntax_dialog.h b/YACReaderLibrary/search_syntax_dialog.h new file mode 100644 index 000000000..52d600c06 --- /dev/null +++ b/YACReaderLibrary/search_syntax_dialog.h @@ -0,0 +1,19 @@ +#ifndef SEARCH_SYNTAX_DIALOG_H +#define SEARCH_SYNTAX_DIALOG_H + +#include + +class SearchSyntaxDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SearchSyntaxDialog(QWidget *parent = nullptr); + +private: + QWidget *createQuickGuideTab(); + QWidget *createFieldsTab(); + QWidget *createExamplesTab(); +}; + +#endif // SEARCH_SYNTAX_DIALOG_H diff --git a/YACReaderLibrary/server/controllers/v2/comiccontroller_v2.cpp b/YACReaderLibrary/server/controllers/v2/comiccontroller_v2.cpp index 7c71efc0d..31b98537a 100644 --- a/YACReaderLibrary/server/controllers/v2/comiccontroller_v2.cpp +++ b/YACReaderLibrary/server/controllers/v2/comiccontroller_v2.cpp @@ -5,7 +5,6 @@ #include "comic.h" #include "comic_db.h" #include "db_helper.h" -#include "qnaturalsorting.h" #include "yacreader_http_session.h" #include "yacreader_libraries.h" @@ -91,9 +90,10 @@ void ComicControllerV2::service(HttpRequest &request, HttpResponse &response) response.write(QString("libraryId:%1\r\n").arg(libraryId).toUtf8()); if (remoteComic) // send previous and next comics id { - QList siblings = DBHelper::getFolderComicsFromLibrary(libraryId, comic.parentId, false); - - std::sort(siblings.begin(), siblings.end(), LibraryItemSorter()); + // Same reading order the desktop reader walks with next/previous, and the + // one the iOS client sorts a folder's comics with, so previousComic/nextComic + // agree with what the user sees listed. + QList siblings = DBHelper::getFolderComicsFromLibraryForReading(libraryId, comic.parentId); bool found = false; int i; diff --git a/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.cpp b/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.cpp index 335a615b3..6f5508ae1 100644 --- a/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.cpp +++ b/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.cpp @@ -47,6 +47,22 @@ std::optional requestUpdate(Lib return result; } +std::optional requestXmlRescan(LibrariesUpdateCoordinator *coordinator, int libraryId) +{ + LibrariesUpdateCoordinator::UpdateRequestResult result = LibrariesUpdateCoordinator::UpdateRequestResult::NotAllowed; + const auto request = [coordinator, libraryId, &result] { + result = coordinator->requestSingleLibraryXmlRescan(libraryId); + }; + + if (coordinator->thread() == QThread::currentThread()) { + request(); + } else if (!QMetaObject::invokeMethod(coordinator, request, Qt::BlockingQueuedConnection)) { + return std::nullopt; + } + + return result; +} + } // namespace UpdateLibrariesControllerV2::UpdateLibrariesControllerV2() { } @@ -91,6 +107,30 @@ void UpdateLibrariesControllerV2::service(HttpRequest &request, HttpResponse &re return; } + QRegExp xmlRescan("/v2/library/([0-9]+)/rescan-xml/?"); + if (xmlRescan.exactMatch(QString::fromUtf8(path))) { + const auto result = requestXmlRescan(coordinator, xmlRescan.cap(1).toInt()); + if (!result.has_value()) { + writeJson(response, 503, "Service Unavailable", { { "error", "updates_unavailable" } }); + return; + } + + switch (result.value()) { + case LibrariesUpdateCoordinator::UpdateRequestResult::Started: + writeJson(response, 202, "Accepted", { { "status", "started" }, { "running", true } }); + return; + case LibrariesUpdateCoordinator::UpdateRequestResult::AlreadyRunning: + writeJson(response, 409, "Conflict", { { "status", "already_running" }, { "running", true } }); + return; + case LibrariesUpdateCoordinator::UpdateRequestResult::NotAllowed: + writeJson(response, 409, "Conflict", { { "status", "update_not_allowed" }, { "running", false } }); + return; + case LibrariesUpdateCoordinator::UpdateRequestResult::LibraryNotFound: + writeJson(response, 404, "Not Found", { { "error", "library_not_found" } }); + return; + } + } + QRegExp singleLibrary("/v2/library/([0-9]+)/update/?"); std::optional libraryId; if (singleLibrary.exactMatch(QString::fromUtf8(path))) { diff --git a/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.h b/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.h index c6a30f96e..a0bcb9da4 100644 --- a/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.h +++ b/YACReaderLibrary/server/controllers/v2/updatelibrariescontroller_v2.h @@ -16,6 +16,7 @@ Routes (dispatched by RequestMapper): - POST /v2/libraries/update -> update all libraries - POST /v2/library//update -> update a single library + - POST /v2/library//rescan-xml -> rescan XML metadata for a single library - GET /v2/libraries/update/status -> { "running": bool } - POST /v2/libraries/update/cancel -> cancel a running update */ diff --git a/YACReaderLibrary/server/controllers/webui/webuicontroller.cpp b/YACReaderLibrary/server/controllers/webui/webuicontroller.cpp index 7b5a81ae7..b2c01aece 100644 --- a/YACReaderLibrary/server/controllers/webui/webuicontroller.cpp +++ b/YACReaderLibrary/server/controllers/webui/webuicontroller.cpp @@ -53,7 +53,7 @@ void setPageHeaders(HttpResponse &response) response.setHeader("Content-Type", "text/html; charset=utf-8"); response.setHeader("Connection", "close"); response.setHeader("Cache-Control", "no-store"); - response.setHeader("Content-Security-Policy", "default-src 'self'; img-src 'self'; style-src 'self'; script-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"); + response.setHeader("Content-Security-Policy", "default-src 'self'; img-src 'self' blob:; style-src 'self'; script-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"); response.setHeader("Referrer-Policy", "no-referrer"); response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("X-Frame-Options", "DENY"); @@ -92,7 +92,7 @@ void WebUIController::service(HttpRequest &request, HttpResponse &response) } const QRegularExpression libraryBrowserPath( - QStringLiteral(R"(^/webui/library/([0-9]+)(?:/(folder|comic)/([0-9]+))?/?$)")); + QStringLiteral(R"(^/webui/library/([0-9]+)(?:/(folder|comic)/([0-9]+)(?:/(read))?)?/?$)")); const QRegularExpressionMatch libraryBrowserMatch = libraryBrowserPath.match(QString::fromUtf8(path)); if (libraryBrowserMatch.hasMatch()) { if (method != "GET") { @@ -110,7 +110,18 @@ void WebUIController::service(HttpRequest &request, HttpResponse &response) return; } - const QString initialView = libraryBrowserMatch.captured(2).isEmpty() ? QStringLiteral("folder") : libraryBrowserMatch.captured(2); + if (!libraryBrowserMatch.captured(4).isEmpty() && libraryBrowserMatch.captured(2) != QStringLiteral("comic")) { + response.setStatus(404, "Not Found"); + response.setHeader("Content-Type", "text/plain; charset=utf-8"); + response.write("404 invalid reader path", true); + return; + } + + const QString initialView = !libraryBrowserMatch.captured(4).isEmpty() + ? QStringLiteral("reader") + : libraryBrowserMatch.captured(2).isEmpty() + ? QStringLiteral("folder") + : libraryBrowserMatch.captured(2); bool itemIdIsValid = false; qulonglong initialItemId = libraryBrowserMatch.captured(3).toULongLong(&itemIdIsValid); if (libraryBrowserMatch.captured(3).isEmpty()) { @@ -311,6 +322,13 @@ void WebUIController::renderStatusPage(HttpRequest &request, HttpResponse &respo Update library + @@ -403,7 +421,7 @@ void WebUIController::renderLibraryBrowser(HttpRequest &request, Status - + diff --git a/YACReaderLibrary/server/requestmapper.cpp b/YACReaderLibrary/server/requestmapper.cpp index 6f1703166..f7085e579 100644 --- a/YACReaderLibrary/server/requestmapper.cpp +++ b/YACReaderLibrary/server/requestmapper.cpp @@ -117,6 +117,7 @@ void RequestMapper::serviceV2(HttpRequest &request, HttpResponse &response) QRegExp librariesUpdateStatus("/v2/libraries/update/status/?"); // poll whether an update is running QRegExp librariesUpdateCancel("/v2/libraries/update/cancel/?"); // cancel a running update QRegExp libraryUpdate("/v2/library/[0-9]+/update/?"); // trigger an update of a single library + QRegExp libraryXmlRescan("/v2/library/[0-9]+/rescan-xml/?"); // rescan ComicInfo.xml metadata for a single library QRegExp library("/v2/library/([0-9]+)/.+"); // permite verificar que la biblioteca solicitada existe @@ -160,7 +161,7 @@ void RequestMapper::serviceV2(HttpRequest &request, HttpResponse &response) if (!updateController.error) { emit comicUpdated(updateController.updatedLibraryId, updateController.updatedComicId); } - } else if (libraryUpdate.exactMatch(path)) { + } else if (libraryUpdate.exactMatch(path) || libraryXmlRescan.exactMatch(path)) { UpdateLibrariesControllerV2().service(request, response); } else if (folderContent.exactMatch(path)) { FolderContentControllerV2().service(request, response); diff --git a/YACReaderLibrary/server_config_dialog.cpp b/YACReaderLibrary/server_config_dialog.cpp index 21a361469..7a314bf29 100644 --- a/YACReaderLibrary/server_config_dialog.cpp +++ b/YACReaderLibrary/server_config_dialog.cpp @@ -5,218 +5,309 @@ #include "yacreader_global_gui.h" #include "yacreader_http_server.h" -#include +#include +#include +#include +#include #include +#include +#include #include #include -#include #include +#include +#include +#include extern YACReaderHttpServer *httpServer; ServerConfigDialog::ServerConfigDialog(QWidget *parent) : QDialog(parent) { - // Background decoration (SVG on left side) + setWindowTitle(tr("Server connectivity")); + setFixedSize(770, 595); + backgroundDecoration = new QLabel(this); - backgroundDecoration->move(0, 0); - backgroundDecoration->setFixedSize(329, 595); + backgroundDecoration->setGeometry(0, 0, 329, 595); backgroundDecoration->setScaledContents(true); - accept = new QPushButton(tr("set port"), this); - qrCode = new QLabel(this); - qrCode->move(64, 112); - qrCode->setFixedSize(200, 200); + qrCode->setGeometry(64, 112, 200, 200); qrCode->setScaledContents(true); - titleLabel = new QLabel(tr("Server connectivity information"), this); - titleLabel->move(332, 61); - - qrMessageLabel = new QLabel(tr("Scan it!"), this); - qrMessageLabel->move(135, 388); + qrMessageLabel = new QLabel(tr("Scan to connect"), this); + qrMessageLabel->setGeometry(43, 312, 243, 208); + qrMessageLabel->setAlignment(Qt::AlignCenter); qrMessageLabel->setWordWrap(true); - qrMessageLabel->setFixedWidth(200); - propagandaLabel = new QLabel(tr("YACReader is available for iOS and Android devices.
Discover it for
iOS or Android."), this); - propagandaLabel->move(332, 505); + auto detailsWidget = new QWidget(this); + detailsWidget->setGeometry(332, 0, 410, 595); + + auto detailsLayout = new QVBoxLayout(detailsWidget); + detailsLayout->setContentsMargins(0, 48, 0, 43); + detailsLayout->setSpacing(0); + + titleLabel = new QLabel(tr("Server connectivity"), detailsWidget); + detailsLayout->addWidget(titleLabel); + detailsLayout->addSpacing(7); + + descriptionLabel = new QLabel(tr("Devices on this network can reach your library at the address below."), detailsWidget); + descriptionLabel->setWordWrap(true); + detailsLayout->addWidget(descriptionLabel); + detailsLayout->addSpacing(25); + + auto formLayout = new QGridLayout; + formLayout->setContentsMargins(0, 0, 0, 0); + formLayout->setHorizontalSpacing(0); + formLayout->setVerticalSpacing(7); + formLayout->setColumnStretch(0, 9); + formLayout->setColumnMinimumWidth(1, 16); + formLayout->setColumnStretch(2, 4); + formLayout->setColumnMinimumWidth(3, 7); + + ipLabel = new QLabel(tr("IP address"), detailsWidget); + portLabel = new QLabel(tr("Port"), detailsWidget); + formLayout->addWidget(ipLabel, 0, 0); + formLayout->addWidget(portLabel, 0, 2); + + ip = new QComboBox(detailsWidget); + ip->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + ip->setMinimumWidth(100); + port = new QLineEdit(QStringLiteral("8080"), detailsWidget); + port->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + port->setMinimumWidth(55); + port->setValidator(new QIntValidator(1024, 65535, this)); + accept = new QPushButton(tr("Set port"), detailsWidget); + accept->setObjectName(QStringLiteral("primaryButton")); + accept->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + + formLayout->addWidget(ip, 1, 0); + formLayout->addWidget(port, 1, 2); + formLayout->addWidget(accept, 1, 4); + detailsLayout->addLayout(formLayout); + detailsLayout->addSpacing(26); + + connectionCard = new QFrame(detailsWidget); + connectionCard->setObjectName(QStringLiteral("connectionCard")); + auto cardLayout = new QVBoxLayout(connectionCard); + cardLayout->setContentsMargins(16, 14, 16, 14); + cardLayout->setSpacing(5); + + webInterfaceLabel = new QLabel(tr("Web interface").toUpper(), connectionCard); + webInterfaceLabel->setObjectName(QStringLiteral("sectionLabel")); + cardLayout->addWidget(webInterfaceLabel); + + webInterfaceUrlLabel = new QLabel(connectionCard); + webInterfaceUrlLabel->setObjectName(QStringLiteral("webInterfaceUrl")); + webInterfaceUrlLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + cardLayout->addWidget(webInterfaceUrlLabel); + cardLayout->addSpacing(7); + + auto buttonsLayout = new QHBoxLayout; + buttonsLayout->setContentsMargins(0, 0, 0, 0); + buttonsLayout->setSpacing(8); + buttonsLayout->addStretch(); + + copyLinkButton = new QPushButton(tr("Copy link"), connectionCard); + copyLinkButton->setObjectName(QStringLiteral("secondaryButton")); + openWebUiButton = new QPushButton(tr("Open web UI"), connectionCard); + openWebUiButton->setObjectName(QStringLiteral("primaryButton")); + buttonsLayout->addWidget(copyLinkButton); + buttonsLayout->addWidget(openWebUiButton); + cardLayout->addLayout(buttonsLayout); + detailsLayout->addWidget(connectionCard); + detailsLayout->addSpacing(25); + + check = new QCheckBox(tr("Enable the server"), detailsWidget); + detailsLayout->addWidget(check); + detailsLayout->addStretch(); + + propagandaText = tr("YACReader is available for iOS and Android. Discover it for iOS or Android."); + propagandaLabel = new QLabel(detailsWidget); propagandaLabel->setOpenExternalLinks(true); + propagandaLabel->setWordWrap(true); + detailsLayout->addWidget(propagandaLabel); - // FORM--------------------------------------------------------------------- - - ipLabel = new QLabel(tr("Choose an IP address"), this); - ipLabel->move(332, 117); - - portLabel = new QLabel(tr("Port"), this); - portLabel->move(332, 211); - - ip = new QComboBox(this); connect(ip, &QComboBox::currentTextChanged, this, &ServerConfigDialog::regenerateQR); - - ip->setFixedWidth(200); - ip->move(332, 153); - - port = new QLineEdit("8080", this); - port->setReadOnly(false); - - connect(port, &QLineEdit::textChanged, this, [=](const QString &portValue) { - accept->setEnabled(!portValue.isEmpty()); + connect(port, &QLineEdit::textChanged, this, [this] { + accept->setEnabled(check->isChecked() && port->hasAcceptableInput() && port->text() != httpServer->getPort()); + }); + connect(accept, &QPushButton::clicked, this, &ServerConfigDialog::updatePort); + connect(copyLinkButton, &QPushButton::clicked, this, [this] { + QApplication::clipboard()->setText(webInterfaceUrl()); + }); + connect(openWebUiButton, &QPushButton::clicked, this, [this] { + QDesktopServices::openUrl(QUrl(webInterfaceUrl())); }); - QValidator *validator = new QIntValidator(1024, 65535, this); - port->setValidator(validator); - - QWidget *portWidget = new QWidget(this); - auto portWidgetLayout = new QHBoxLayout(this); - portWidgetLayout->addWidget(port); - portWidgetLayout->addWidget(accept); - portWidgetLayout->setContentsMargins(0, 0, 0, 0); - portWidget->setLayout(portWidgetLayout); - portWidget->move(332, 244); - connect(accept, &QAbstractButton::pressed, this, &ServerConfigDialog::updatePort); - - // END FORM----------------------------------------------------------------- - - check = new QCheckBox(this); - check->move(332, 314); - check->setText(tr("enable the server")); - - this->setFixedSize(770, 595); + QSettings settings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); + settings.beginGroup(QStringLiteral("libraryConfig")); + const bool serverEnabled = settings.value(SERVER_ON, true).toBool(); + settings.endGroup(); - QSettings *settings = new QSettings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); - settings->beginGroup("libraryConfig"); + initTheme(this); + accept->setMinimumWidth(qMax(102, accept->sizeHint().width())); - if (settings->value(SERVER_ON, true).toBool()) { - check->setChecked(true); + check->setChecked(serverEnabled); + connect(check, &QCheckBox::stateChanged, this, &ServerConfigDialog::enableServer); + setConnectionControlsEnabled(serverEnabled); + if (serverEnabled) generateQR(); - } else { - ip->setDisabled(true); - port->setDisabled(true); - accept->setDisabled(true); - - check->setChecked(false); + else { + qrCode->clear(); + refreshWebInterface(); } - - settings->endGroup(); - - connect(check, &QCheckBox::stateChanged, this, &ServerConfigDialog::enableServer); - - // Initialize theme - initTheme(this); } void ServerConfigDialog::applyTheme(const Theme &theme) { - // Apply pre-built QSS from theme setStyleSheet(theme.serverConfigDialog.dialogQSS); titleLabel->setStyleSheet(theme.serverConfigDialog.titleLabelQSS); qrMessageLabel->setStyleSheet(theme.serverConfigDialog.qrMessageLabelQSS); propagandaLabel->setStyleSheet(theme.serverConfigDialog.propagandaLabelQSS); - ipLabel->setStyleSheet(theme.serverConfigDialog.labelQSS); - portLabel->setStyleSheet(theme.serverConfigDialog.labelQSS); + const QColor linkColor = theme.serverConfigDialog.linkColor; + QString themedPropagandaText = propagandaText; + themedPropagandaText.replace(QStringLiteral("setText(themedPropagandaText); + auto propagandaPalette = propagandaLabel->palette(); + propagandaPalette.setColor(QPalette::Link, linkColor); + propagandaPalette.setColor(QPalette::LinkVisited, linkColor); + propagandaLabel->setPalette(propagandaPalette); + descriptionLabel->setStyleSheet(theme.serverConfigDialog.textLabelQSS); + ipLabel->setStyleSheet(theme.serverConfigDialog.secondaryLabelQSS); + portLabel->setStyleSheet(theme.serverConfigDialog.secondaryLabelQSS); check->setStyleSheet(theme.serverConfigDialog.checkBoxQSS); - // Set background decoration (SVG on left) backgroundDecoration->setPixmap(theme.serverConfigDialog.backgroundDecoration); - backgroundDecoration->lower(); // Send to back so other widgets appear on top + backgroundDecoration->lower(); - // Regenerate QR code with new theme colors generateQR(); } void ServerConfigDialog::showEvent(QShowEvent *event) { QDialog::showEvent(event); - generateQR(); } void ServerConfigDialog::enableServer(int status) { - QSettings *settings = new QSettings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); - settings->beginGroup("libraryConfig"); + QSettings settings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); + settings.beginGroup(QStringLiteral("libraryConfig")); - if (status == Qt::Checked) { - ip->setDisabled(false); - port->setDisabled(false); - accept->setDisabled(false); + const bool enabled = status == Qt::Checked; + setConnectionControlsEnabled(enabled); + if (enabled) { httpServer->start(); - this->generateQR(); - settings->setValue(SERVER_ON, true); + generateQR(); } else { httpServer->stop(); - qrCode->setPixmap(QPixmap()); - ip->setDisabled(true); - port->setDisabled(true); - accept->setDisabled(true); - settings->setValue(SERVER_ON, false); + qrCode->clear(); + refreshWebInterface(); } - settings->endGroup(); + settings.setValue(SERVER_ON, enabled); + settings.endGroup(); } void ServerConfigDialog::generateQR() { - if (!httpServer->isRunning()) + if (!httpServer->isRunning()) { + refreshWebInterface(); return; + } + const QSignalBlocker blocker(ip); + const auto addresses = getIpAddresses(); ip->clear(); + ip->addItems(addresses); + port->setText(httpServer->getPort()); - auto addresses = getIpAddresses(); - if (addresses.length() > 0) { + if (!addresses.isEmpty()) generateQR(addresses.first() + ":" + httpServer->getPort()); - ip->addItems(addresses); - port->setText(httpServer->getPort()); - } + else + refreshWebInterface(); } void ServerConfigDialog::generateQR(const QString &serverAddress) { qrCode->clear(); - auto backgroundColor = theme.serverConfigDialog.qrBackgroundColor; - auto foregroundColor = theme.serverConfigDialog.qrForegroundColor; - - qrcodegen::QrCode code = qrcodegen::QrCode::encodeText( + const auto backgroundColor = theme.serverConfigDialog.qrBackgroundColor; + const auto foregroundColor = theme.serverConfigDialog.qrForegroundColor; + const qrcodegen::QrCode code = qrcodegen::QrCode::encodeText( serverAddress.toLocal8Bit(), qrcodegen::QrCode::Ecc::LOW); - int qrSize = code.getSize(); + const int qrSize = code.getSize(); QPixmap qrPixmap(qrSize, qrSize); qrPixmap.fill(backgroundColor); QPainter painter(&qrPixmap); painter.setPen(foregroundColor); painter.setBrush(foregroundColor); - for (int x = 0; x < qrSize; x++) { - for (int y = 0; y < qrSize; y++) { - if (code.getModule(x, y)) { + for (int x = 0; x < qrSize; ++x) { + for (int y = 0; y < qrSize; ++y) { + if (code.getModule(x, y)) painter.drawPoint(x, y); - } } } painter.end(); - // Scale to label size qrPixmap = qrPixmap.scaled(qrCode->size() * devicePixelRatioF(), Qt::KeepAspectRatio, Qt::FastTransformation); qrPixmap.setDevicePixelRatio(devicePixelRatioF()); - qrCode->setPixmap(qrPixmap); + refreshWebInterface(); } -void ServerConfigDialog::regenerateQR(const QString &ip) +void ServerConfigDialog::regenerateQR(const QString &address) { - generateQR(ip + ":" + httpServer->getPort()); + if (httpServer->isRunning() && !address.isEmpty()) + generateQR(address + ":" + httpServer->getPort()); + else + refreshWebInterface(); } void ServerConfigDialog::updatePort() { - QSettings *settings = new QSettings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); // TODO unificar la creación del fichero de config con el servidor - settings->beginGroup("listener"); - settings->setValue("port", port->text().toInt()); - settings->endGroup(); + if (!port->hasAcceptableInput()) { + port->setText(httpServer->getPort()); + return; + } + if (port->text() == httpServer->getPort()) + return; + + QSettings settings(YACReader::getSettingsPath() + "/YACReaderLibrary.ini", QSettings::IniFormat); + settings.beginGroup(QStringLiteral("listener")); + settings.setValue(QStringLiteral("port"), port->text().toInt()); + settings.endGroup(); httpServer->stop(); httpServer->start(); + generateQR(); +} + +QString ServerConfigDialog::webInterfaceUrl() const +{ + if (ip->currentText().isEmpty()) + return { }; + return QStringLiteral("http://%1:%2/webui").arg(ip->currentText(), httpServer->getPort()); +} - generateQR(ip->currentText() + ":" + port->text()); +void ServerConfigDialog::refreshWebInterface() +{ + const QString url = httpServer->isRunning() ? webInterfaceUrl() : QString(); + webInterfaceUrlLabel->setText(url.isEmpty() ? QStringLiteral("—") : url); + copyLinkButton->setEnabled(!url.isEmpty()); + openWebUiButton->setEnabled(!url.isEmpty()); +} + +void ServerConfigDialog::setConnectionControlsEnabled(bool enabled) +{ + ip->setEnabled(enabled); + port->setEnabled(enabled); + accept->setEnabled(enabled && port->hasAcceptableInput() && port->text() != httpServer->getPort()); + if (!enabled) { + copyLinkButton->setEnabled(false); + openWebUiButton->setEnabled(false); + } } diff --git a/YACReaderLibrary/server_config_dialog.h b/YACReaderLibrary/server_config_dialog.h index 67014af44..1367f2b54 100644 --- a/YACReaderLibrary/server_config_dialog.h +++ b/YACReaderLibrary/server_config_dialog.h @@ -10,40 +10,49 @@ #include #include +class QFrame; + class ServerConfigDialog : public QDialog, protected Themable { Q_OBJECT public: - ServerConfigDialog(QWidget *parent = 0); + explicit ServerConfigDialog(QWidget *parent = nullptr); void showEvent(QShowEvent *event) override; protected: void applyTheme(const Theme &theme) override; private: + QString webInterfaceUrl() const; + void refreshWebInterface(); + void setConnectionControlsEnabled(bool enabled); + QComboBox *ip; QLineEdit *port; - QCheckBox *check; - - QPushButton *close; QPushButton *accept; + QPushButton *copyLinkButton; + QPushButton *openWebUiButton; QLabel *qrCode; - - // Labels for themable styling QLabel *titleLabel; + QLabel *descriptionLabel; QLabel *qrMessageLabel; QLabel *propagandaLabel; QLabel *ipLabel; QLabel *portLabel; + QLabel *webInterfaceLabel; + QLabel *webInterfaceUrlLabel; QLabel *backgroundDecoration; + QFrame *connectionCard; + QString propagandaText; public slots: void generateQR(); void generateQR(const QString &serverAddress); - void regenerateQR(const QString &ip); + void regenerateQR(const QString &address); void enableServer(int status); void updatePort(); + signals: void portChanged(QString port); }; diff --git a/YACReaderLibrary/themes/builtin_classic.json b/YACReaderLibrary/themes/builtin_classic.json index 52ac1a37a..ef23c64ba 100644 --- a/YACReaderLibrary/themes/builtin_classic.json +++ b/YACReaderLibrary/themes/builtin_classic.json @@ -225,15 +225,18 @@ "textColor": "#ABABAB" }, "serverConfigDialog": { + "accentColor": "#F7F7F7", + "accentForegroundColor": "#2A2A2A", "backgroundColor": "#2A2A2A", - "checkBoxTextColor": "#262626", - "decorationColor": "#F7F7F7", - "labelTextColor": "#575757", - "propagandaTextColor": "#4D4D4D", + "borderColor": "#7C7C7C", + "linkColor": "#FFCC00", + "propagandaTextColor": "#939393", "qrBackgroundColor": "#2A2A2A", - "qrForegroundColor": "#FFFFFF", + "qrForegroundColor": "#F7F7F7", "qrMessageTextColor": "#A3A3A3", - "titleTextColor": "#474747" + "secondaryTextColor": "#B8B8B8", + "textColor": "#DDDDDD", + "titleTextColor": "#D0D0D0" }, "shortcutsIcons": { "iconColor": "#F7F7F7" diff --git a/YACReaderLibrary/themes/builtin_dark.json b/YACReaderLibrary/themes/builtin_dark.json index 3964ec748..06a60ee54 100644 --- a/YACReaderLibrary/themes/builtin_dark.json +++ b/YACReaderLibrary/themes/builtin_dark.json @@ -225,14 +225,17 @@ "textColor": "#ABABAB" }, "serverConfigDialog": { + "accentColor": "#F7F7F7", + "accentForegroundColor": "#2A2A2A", "backgroundColor": "#2A2A2A", - "checkBoxTextColor": "#DDDDDD", - "decorationColor": "#F7F7F7", - "labelTextColor": "#C0C0C0", + "borderColor": "#7C7C7C", + "linkColor": "#FFCC00", "propagandaTextColor": "#B0B0B0", "qrBackgroundColor": "#2A2A2A", - "qrForegroundColor": "#FFFFFF", + "qrForegroundColor": "#F7F7F7", "qrMessageTextColor": "#A3A3A3", + "secondaryTextColor": "#B8B8B8", + "textColor": "#DDDDDD", "titleTextColor": "#D0D0D0" }, "shortcutsIcons": { diff --git a/YACReaderLibrary/themes/builtin_dark1.json b/YACReaderLibrary/themes/builtin_dark1.json index 59127c345..35808d527 100644 --- a/YACReaderLibrary/themes/builtin_dark1.json +++ b/YACReaderLibrary/themes/builtin_dark1.json @@ -225,14 +225,17 @@ "textColor": "#C1C0C0" }, "serverConfigDialog": { + "accentColor": "#FCFCFA", + "accentForegroundColor": "#2B282D", "backgroundColor": "#2B282D", - "checkBoxTextColor": "#FCFCFA", - "decorationColor": "#FCFCFA", - "labelTextColor": "#D7D6D7", + "borderColor": "#7B7A7C", + "linkColor": "#78DCE8", "propagandaTextColor": "#A6A5A6", - "qrBackgroundColor": "#2D2A2E", - "qrForegroundColor": "#FFFFFF", + "qrBackgroundColor": "#2B282D", + "qrForegroundColor": "#FCFCFA", "qrMessageTextColor": "#A6A5A6", + "secondaryTextColor": "#D1CFD1", + "textColor": "#FCFCFA", "titleTextColor": "#FCFCFA" }, "shortcutsIcons": { diff --git a/YACReaderLibrary/themes/builtin_dark2.json b/YACReaderLibrary/themes/builtin_dark2.json index 0585de39b..a40d37482 100644 --- a/YACReaderLibrary/themes/builtin_dark2.json +++ b/YACReaderLibrary/themes/builtin_dark2.json @@ -225,14 +225,17 @@ "textColor": "#EDEDF0" }, "serverConfigDialog": { + "accentColor": "#F8F8F2", + "accentForegroundColor": "#343746", "backgroundColor": "#343746", - "checkBoxTextColor": "#F8F8F2", - "decorationColor": "#F8F8F2", - "labelTextColor": "#EDEDF0", - "propagandaTextColor": "#6272A4", + "borderColor": "#878A93", + "linkColor": "#8BE9FD", + "propagandaTextColor": "#98A3C4", "qrBackgroundColor": "#343746", "qrForegroundColor": "#F8F8F2", - "qrMessageTextColor": "#6272A4", + "qrMessageTextColor": "#98A3C4", + "secondaryTextColor": "#C9CACF", + "textColor": "#F8F8F2", "titleTextColor": "#F8F8F2" }, "shortcutsIcons": { diff --git a/YACReaderLibrary/themes/builtin_dark3.json b/YACReaderLibrary/themes/builtin_dark3.json index a6d5335c1..c42d8701f 100644 --- a/YACReaderLibrary/themes/builtin_dark3.json +++ b/YACReaderLibrary/themes/builtin_dark3.json @@ -225,14 +225,17 @@ "textColor": "#D5C4A1" }, "serverConfigDialog": { + "accentColor": "#EBDBB2", + "accentForegroundColor": "#32302F", "backgroundColor": "#32302F", - "checkBoxTextColor": "#EBDBB2", - "decorationColor": "#EBDBB2", - "labelTextColor": "#D5C4A1", - "propagandaTextColor": "#928374", + "borderColor": "#8A816D", + "linkColor": "#8EC07C", + "propagandaTextColor": "#A5998C", "qrBackgroundColor": "#32302F", "qrForegroundColor": "#EBDBB2", - "qrMessageTextColor": "#928374", + "qrMessageTextColor": "#A5998C", + "secondaryTextColor": "#C3B494", + "textColor": "#EBDBB2", "titleTextColor": "#EBDBB2" }, "shortcutsIcons": { diff --git a/YACReaderLibrary/themes/builtin_dark4.json b/YACReaderLibrary/themes/builtin_dark4.json index e1144a9ce..be3a3dce7 100644 --- a/YACReaderLibrary/themes/builtin_dark4.json +++ b/YACReaderLibrary/themes/builtin_dark4.json @@ -225,14 +225,17 @@ "textColor": "#E5E9F0" }, "serverConfigDialog": { + "accentColor": "#ECEFF4", + "accentForegroundColor": "#3B4252", "backgroundColor": "#3B4252", - "checkBoxTextColor": "#ECEFF4", - "decorationColor": "#ECEFF4", - "labelTextColor": "#E5E9F0", - "propagandaTextColor": "#81A1C1", + "borderColor": "#9197A2", + "linkColor": "#8FBCBB", + "propagandaTextColor": "#9BB4CE", "qrBackgroundColor": "#3B4252", "qrForegroundColor": "#ECEFF4", - "qrMessageTextColor": "#81A1C1", + "qrMessageTextColor": "#9BB4CE", + "secondaryTextColor": "#C0C5CD", + "textColor": "#ECEFF4", "titleTextColor": "#ECEFF4" }, "shortcutsIcons": { diff --git a/YACReaderLibrary/themes/builtin_light.json b/YACReaderLibrary/themes/builtin_light.json index f6d252ac5..9258223f6 100644 --- a/YACReaderLibrary/themes/builtin_light.json +++ b/YACReaderLibrary/themes/builtin_light.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#606060", + "accentForegroundColor": "#FFFFFF", "backgroundColor": "#FFFFFF", - "checkBoxTextColor": "#262626", - "decorationColor": "#606060", - "labelTextColor": "#575757", + "borderColor": "#8B8B8B", + "linkColor": "#8A6D00", "propagandaTextColor": "#4D4D4D", "qrBackgroundColor": "#FFFFFF", "qrForegroundColor": "#606060", - "qrMessageTextColor": "#A3A3A3", - "titleTextColor": "#474747" + "qrMessageTextColor": "#747474", + "secondaryTextColor": "#575757", + "textColor": "#262626", + "titleTextColor": "#606060" }, "shortcutsIcons": { "iconColor": "#606060" diff --git a/YACReaderLibrary/themes/builtin_light1.json b/YACReaderLibrary/themes/builtin_light1.json index 662d8ac39..445a5b0eb 100644 --- a/YACReaderLibrary/themes/builtin_light1.json +++ b/YACReaderLibrary/themes/builtin_light1.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#66697D", + "accentForegroundColor": "#EFF1F5", "backgroundColor": "#EFF1F5", - "checkBoxTextColor": "#4C4F69", - "decorationColor": "#6C6F85", - "labelTextColor": "#5C5F77", - "propagandaTextColor": "#6C6F85", - "qrBackgroundColor": "#FFFFFF", - "qrForegroundColor": "#4C4F69", - "qrMessageTextColor": "#8C8FA1", - "titleTextColor": "#303446" + "borderColor": "#7E8095", + "linkColor": "#5269A8", + "propagandaTextColor": "#686A7F", + "qrBackgroundColor": "#EFF1F5", + "qrForegroundColor": "#66697D", + "qrMessageTextColor": "#686B78", + "secondaryTextColor": "#63667D", + "textColor": "#4C4F69", + "titleTextColor": "#6C6F85" }, "shortcutsIcons": { "iconColor": "#5C5F77" diff --git a/YACReaderLibrary/themes/builtin_light2.json b/YACReaderLibrary/themes/builtin_light2.json index cdff8ce64..2c39672c8 100644 --- a/YACReaderLibrary/themes/builtin_light2.json +++ b/YACReaderLibrary/themes/builtin_light2.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#4A5685", + "accentForegroundColor": "#E1E2E7", "backgroundColor": "#E1E2E7", - "checkBoxTextColor": "#2F334D", - "decorationColor": "#4A5685", - "labelTextColor": "#4A5685", - "propagandaTextColor": "#68709A", - "qrBackgroundColor": "#FFFFFF", - "qrForegroundColor": "#3760BF", - "qrMessageTextColor": "#8990B3", - "titleTextColor": "#1F2335" + "borderColor": "#6D779C", + "linkColor": "#1C5FB8", + "propagandaTextColor": "#5A6085", + "qrBackgroundColor": "#E1E2E7", + "qrForegroundColor": "#4A5685", + "qrMessageTextColor": "#5D6179", + "secondaryTextColor": "#4A5685", + "textColor": "#2F334D", + "titleTextColor": "#4A5685" }, "shortcutsIcons": { "iconColor": "#4A5685" diff --git a/YACReaderLibrary/themes/builtin_light3.json b/YACReaderLibrary/themes/builtin_light3.json index a6fd68a82..157f3e9c7 100644 --- a/YACReaderLibrary/themes/builtin_light3.json +++ b/YACReaderLibrary/themes/builtin_light3.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#626F77", + "accentForegroundColor": "#FDF6E3", "backgroundColor": "#FDF6E3", - "checkBoxTextColor": "#4F5B58", - "decorationColor": "#708089", - "labelTextColor": "#708089", - "propagandaTextColor": "#859289", - "qrBackgroundColor": "#FFFBEF", - "qrForegroundColor": "#5C6A72", - "qrMessageTextColor": "#9DA9A0", - "titleTextColor": "#3F4B47" + "borderColor": "#7E8786", + "linkColor": "#24729C", + "propagandaTextColor": "#67716A", + "qrBackgroundColor": "#FDF6E3", + "qrForegroundColor": "#626F77", + "qrMessageTextColor": "#68706A", + "secondaryTextColor": "#606E75", + "textColor": "#4F5B58", + "titleTextColor": "#708089" }, "shortcutsIcons": { "iconColor": "#708089" diff --git a/YACReaderLibrary/themes/builtin_light4.json b/YACReaderLibrary/themes/builtin_light4.json index d9619ff9c..ec586eeee 100644 --- a/YACReaderLibrary/themes/builtin_light4.json +++ b/YACReaderLibrary/themes/builtin_light4.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#6D6984", + "accentForegroundColor": "#FAF4ED", "backgroundColor": "#FAF4ED", - "checkBoxTextColor": "#575279", - "decorationColor": "#6E6A86", - "labelTextColor": "#6E6A86", - "propagandaTextColor": "#797593", - "qrBackgroundColor": "#FFF8F2", - "qrForegroundColor": "#575279", - "qrMessageTextColor": "#A59EAF", - "titleTextColor": "#4F4A72" + "borderColor": "#878199", + "linkColor": "#286983", + "propagandaTextColor": "#6E6B86", + "qrBackgroundColor": "#FAF4ED", + "qrForegroundColor": "#6D6984", + "qrMessageTextColor": "#706C77", + "secondaryTextColor": "#6B6883", + "textColor": "#575279", + "titleTextColor": "#6E6A86" }, "shortcutsIcons": { "iconColor": "#6E6A86" diff --git a/YACReaderLibrary/themes/builtin_light5.json b/YACReaderLibrary/themes/builtin_light5.json index f6fbaa3a8..604627109 100644 --- a/YACReaderLibrary/themes/builtin_light5.json +++ b/YACReaderLibrary/themes/builtin_light5.json @@ -225,15 +225,18 @@ "textColor": "#FFFFFF" }, "serverConfigDialog": { + "accentColor": "#6D6B5E", + "accentForegroundColor": "#F7F2E7", "backgroundColor": "#F7F2E7", - "checkBoxTextColor": "#545464", - "decorationColor": "#716E61", - "labelTextColor": "#716E61", - "propagandaTextColor": "#8A8980", - "qrBackgroundColor": "#FFF9F0", - "qrForegroundColor": "#545464", - "qrMessageTextColor": "#A6A294", - "titleTextColor": "#43436C" + "borderColor": "#86827C", + "linkColor": "#4D699B", + "propagandaTextColor": "#6D6C65", + "qrBackgroundColor": "#F7F2E7", + "qrForegroundColor": "#6D6B5E", + "qrMessageTextColor": "#6F6C63", + "secondaryTextColor": "#6C6A5D", + "textColor": "#545464", + "titleTextColor": "#716E61" }, "shortcutsIcons": { "iconColor": "#716E61" diff --git a/YACReaderLibrary/themes/theme.h b/YACReaderLibrary/themes/theme.h index 24d10ca77..57719ffca 100644 --- a/YACReaderLibrary/themes/theme.h +++ b/YACReaderLibrary/themes/theme.h @@ -342,6 +342,7 @@ struct SearchLineEditTheme { QString lineEditQSS; QString searchLabelQSS; QString clearButtonQSS; + QColor iconColor; QPixmap searchIcon; QPixmap clearIcon; }; @@ -401,8 +402,10 @@ struct ServerConfigDialogTheme { QString titleLabelQSS; QString qrMessageLabelQSS; QString propagandaLabelQSS; - QString labelQSS; + QString textLabelQSS; + QString secondaryLabelQSS; QString checkBoxQSS; + QColor linkColor; QColor qrBackgroundColor; QColor qrForegroundColor; QPixmap backgroundDecoration; diff --git a/YACReaderLibrary/themes/theme_factory.cpp b/YACReaderLibrary/themes/theme_factory.cpp index 9ecf3e6db..11ead49c9 100644 --- a/YACReaderLibrary/themes/theme_factory.cpp +++ b/YACReaderLibrary/themes/theme_factory.cpp @@ -239,12 +239,33 @@ struct SidebarIconsParams { }; struct ServerConfigDialogThemeTemplates { - QString dialogQSS = "ServerConfigDialog { background-color: %1; }"; - QString titleLabelQSS = "QLabel { color: %1; font-size: 30px; font-family: Arial; }"; - QString qrMessageLabelQSS = "QLabel { color: %1; font-size: 18px; font-family: Arial; }"; - QString propagandaLabelQSS = "QLabel { color: %1; font-size: 13px; font-family: Arial; font-style: italic; }"; - QString labelQSS = "QLabel { color: %1; font-size: 18px; font-family: Arial; }"; - QString checkBoxQSS = "QCheckBox { color: %1; font-size: 13px; font-family: Arial; }"; + // %1 = background, %2 = text, %3 = border, %4 = accent, %5 = card background, + // %6 = combo-box chevron, %7 = secondary text, %8 = accent foreground, + // %9 = disabled combo-box chevron + QString dialogQSS = "ServerConfigDialog { background-color: %1; }" + "QComboBox, QLineEdit { background-color: %1; color: %2; border: 1px solid %3; border-radius: 5px; padding: 7px 10px; min-height: 18px; }" + "QComboBox { padding-right: 28px; }" + "QComboBox::drop-down { subcontrol-origin: border; subcontrol-position: top right; width: 28px; border: none; background: transparent; }" + "QComboBox::down-arrow { image: url('%6'); width: 10px; height: 6px; }" + "QComboBox:focus, QLineEdit:focus { border-color: %4; }" + "QComboBox:disabled, QLineEdit:disabled { color: %7; background-color: %5; }" + "QComboBox::down-arrow:disabled { image: url('%9'); }" + "QFrame#connectionCard { background-color: %5; border: 1px solid %3; border-radius: 8px; }" + "QLabel#sectionLabel { color: %7; border: none; font-size: 11px; font-weight: bold; }" + "QLabel#webInterfaceUrl { color: %2; border: none; font-family: monospace; font-size: 12px; }" + "QPushButton { min-height: 32px; padding: 0 13px; border-radius: 5px; font-size: 12px; }" + "QPushButton#secondaryButton { color: %2; background: transparent; border: 1px solid %3; }" + "QPushButton#secondaryButton:hover { border-color: %4; }" + "QPushButton#primaryButton { color: %8; background-color: %4; border: 1px solid %4; font-weight: bold; }" + "QPushButton:disabled { color: %7; background: transparent; border-color: %3; }"; + QString titleLabelQSS = "QLabel { color: %1; font-size: 28px; font-weight: bold; }"; + QString qrMessageLabelQSS = "QLabel { color: %1; font-size: 16px; }"; + QString propagandaLabelQSS = "QLabel { color: %1; font-size: 15px; font-style: italic; }"; + QString textLabelQSS = "QLabel { color: %1; font-size: 12px; }"; + QString secondaryLabelQSS = "QLabel { color: %1; font-size: 12px; }"; + QString checkBoxQSS = "QCheckBox { color: %1; font-size: 13px; spacing: 9px; }" + "QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid %2; border-radius: 3px; background: transparent; }" + "QCheckBox::indicator:checked { background-color: %2; image: url(%3); }"; }; struct LibraryItemParams { @@ -318,11 +339,14 @@ struct ServerConfigDialogParams { QColor titleTextColor; QColor qrMessageTextColor; QColor propagandaTextColor; - QColor labelTextColor; - QColor checkBoxTextColor; + QColor textColor; + QColor secondaryTextColor; + QColor borderColor; + QColor accentColor; + QColor accentForegroundColor; + QColor linkColor; QColor qrBackgroundColor; QColor qrForegroundColor; - QColor decorationColor; }; struct WhatsNewDialogParams { @@ -816,6 +840,7 @@ Theme makeTheme(const ThemeParams ¶ms) sle.backgroundColor.name()); theme.searchLineEdit.searchLabelQSS = sle.t.searchLabelQSS; theme.searchLineEdit.clearButtonQSS = sle.t.clearButtonQSS; + theme.searchLineEdit.iconColor = sle.iconColor; const qreal dpr = qApp->devicePixelRatio(); theme.searchLineEdit.searchIcon = renderSvgToPixmap(recoloredSvgToThemeFile(":/images/iconSearchNew.svg", sle.iconColor, params.meta.id), 15, dpr); @@ -928,15 +953,19 @@ Theme makeTheme(const ThemeParams ¶ms) // ServerConfigDialog const auto &scd = params.serverConfigDialogParams; - theme.serverConfigDialog.dialogQSS = scd.t.dialogQSS.arg(scd.backgroundColor.name()); + QColor cardColor = scd.backgroundColor; + cardColor = cardColor.darker(cardColor.lightness() > 127 ? 104 : 112); + theme.serverConfigDialog.dialogQSS = scd.t.dialogQSS.arg(scd.backgroundColor.name(), scd.textColor.name(), scd.borderColor.name(), scd.accentColor.name(), cardColor.name(), recolor(":/images/chevronDown.svg", scd.accentColor), scd.secondaryTextColor.name(), scd.accentForegroundColor.name(), recolor(":/images/chevronDown.svg", scd.secondaryTextColor)); theme.serverConfigDialog.titleLabelQSS = scd.t.titleLabelQSS.arg(scd.titleTextColor.name()); theme.serverConfigDialog.qrMessageLabelQSS = scd.t.qrMessageLabelQSS.arg(scd.qrMessageTextColor.name()); theme.serverConfigDialog.propagandaLabelQSS = scd.t.propagandaLabelQSS.arg(scd.propagandaTextColor.name()); - theme.serverConfigDialog.labelQSS = scd.t.labelQSS.arg(scd.labelTextColor.name()); - theme.serverConfigDialog.checkBoxQSS = scd.t.checkBoxQSS.arg(scd.checkBoxTextColor.name()); + theme.serverConfigDialog.textLabelQSS = scd.t.textLabelQSS.arg(scd.textColor.name()); + theme.serverConfigDialog.secondaryLabelQSS = scd.t.secondaryLabelQSS.arg(scd.secondaryTextColor.name()); + theme.serverConfigDialog.checkBoxQSS = scd.t.checkBoxQSS.arg(scd.textColor.name(), scd.accentColor.name(), recolor(":/images/comic_vine/checkBoxTick.svg", scd.accentForegroundColor)); + theme.serverConfigDialog.linkColor = scd.linkColor; theme.serverConfigDialog.qrBackgroundColor = scd.qrBackgroundColor; theme.serverConfigDialog.qrForegroundColor = scd.qrForegroundColor; - theme.serverConfigDialog.backgroundDecoration = QPixmap(recoloredSvgToThemeFile(":/images/serverConfigBackground.svg", scd.decorationColor, params.meta.id)); + theme.serverConfigDialog.backgroundDecoration = QPixmap(recoloredSvgToThemeFile(":/images/serverConfigBackground.svg", scd.accentColor, params.meta.id)); theme.meta = params.meta; @@ -1097,11 +1126,14 @@ Theme makeTheme(const QJsonObject &json) scd2.titleTextColor = colorFromJson(o, "titleTextColor", scd2.titleTextColor); scd2.qrMessageTextColor = colorFromJson(o, "qrMessageTextColor", scd2.qrMessageTextColor); scd2.propagandaTextColor = colorFromJson(o, "propagandaTextColor", scd2.propagandaTextColor); - scd2.labelTextColor = colorFromJson(o, "labelTextColor", scd2.labelTextColor); - scd2.checkBoxTextColor = colorFromJson(o, "checkBoxTextColor", scd2.checkBoxTextColor); + scd2.textColor = colorFromJson(o, "textColor", scd2.textColor); + scd2.secondaryTextColor = colorFromJson(o, "secondaryTextColor", scd2.secondaryTextColor); + scd2.borderColor = colorFromJson(o, "borderColor", scd2.borderColor); + scd2.accentColor = colorFromJson(o, "accentColor", scd2.accentColor); + scd2.accentForegroundColor = colorFromJson(o, "accentForegroundColor", scd2.accentForegroundColor); + scd2.linkColor = colorFromJson(o, "linkColor", scd2.linkColor); scd2.qrBackgroundColor = colorFromJson(o, "qrBackgroundColor", scd2.qrBackgroundColor); scd2.qrForegroundColor = colorFromJson(o, "qrForegroundColor", scd2.qrForegroundColor); - scd2.decorationColor = colorFromJson(o, "decorationColor", scd2.decorationColor); } if (json.contains("mainToolbar")) { diff --git a/YACReaderLibrary/xml_info_parser.cpp b/YACReaderLibrary/xml_info_parser.cpp index 75115024b..4b8805941 100644 --- a/YACReaderLibrary/xml_info_parser.cpp +++ b/YACReaderLibrary/xml_info_parser.cpp @@ -167,6 +167,48 @@ bool tryValues(QXmlStreamReader &reader, ComicInfo &info) return false; } +const QList &YACReader::xmlMetadataFields() +{ + static const QList fields = { + { "title", &ComicInfo::title }, + { "number", &ComicInfo::number }, + { "count", &ComicInfo::count }, + { "volume", &ComicInfo::volume }, + { "storyArc", &ComicInfo::storyArc }, + { "genere", &ComicInfo::genere }, + { "writer", &ComicInfo::writer }, + { "penciller", &ComicInfo::penciller }, + { "inker", &ComicInfo::inker }, + { "colorist", &ComicInfo::colorist }, + { "letterer", &ComicInfo::letterer }, + { "coverArtist", &ComicInfo::coverArtist }, + { "date", &ComicInfo::date }, + { "publisher", &ComicInfo::publisher }, + { "format", &ComicInfo::format }, + { "color", &ComicInfo::color }, + { "ageRating", &ComicInfo::ageRating }, + { "synopsis", &ComicInfo::synopsis }, + { "characters", &ComicInfo::characters }, + { "notes", &ComicInfo::notes }, + { "comicVineID", &ComicInfo::comicVineID }, + { "type", &ComicInfo::type }, + { "editor", &ComicInfo::editor }, + { "imprint", &ComicInfo::imprint }, + { "teams", &ComicInfo::teams }, + { "locations", &ComicInfo::locations }, + { "series", &ComicInfo::series }, + { "alternateSeries", &ComicInfo::alternateSeries }, + { "alternateNumber", &ComicInfo::alternateNumber }, + { "alternateCount", &ComicInfo::alternateCount }, + { "languageISO", &ComicInfo::languageISO }, + { "seriesGroup", &ComicInfo::seriesGroup }, + { "mainCharacterOrTeam", &ComicInfo::mainCharacterOrTeam }, + { "review", &ComicInfo::review }, + }; + + return fields; +} + bool YACReader::parseXMLIntoInfo(const QByteArray &xmlRawData, ComicInfo &info) { if (xmlRawData.isEmpty()) { diff --git a/YACReaderLibrary/xml_info_parser.h b/YACReaderLibrary/xml_info_parser.h index 295aba756..7de9aa452 100644 --- a/YACReaderLibrary/xml_info_parser.h +++ b/YACReaderLibrary/xml_info_parser.h @@ -3,10 +3,22 @@ #include "comic_db.h" +#include + namespace YACReader { bool parseXMLIntoInfo(const QByteArray &xmlRawData, ComicInfo &info); +// Every comic_info column that parseXMLIntoInfo can set, so code that writes +// parsed metadata to the DB stays in sync with the parser. When the parser +// learns a new field, add its column here too. +struct XmlMetadataField { + const char *column; + QVariant ComicInfo::*member; +}; + +const QList &xmlMetadataFields(); + } #endif // XMLINFOPARSER_H diff --git a/YACReaderLibrary/yacreader_content_views_manager.cpp b/YACReaderLibrary/yacreader_content_views_manager.cpp index e4276cf6c..6abacc2a5 100644 --- a/YACReaderLibrary/yacreader_content_views_manager.cpp +++ b/YACReaderLibrary/yacreader_content_views_manager.cpp @@ -235,7 +235,7 @@ void YACReaderContentViewsManager::switchToComicsView(ComicsView *from, ComicsVi // load content into current view libraryWindow->loadCoversFromCurrentModel(); - if (!libraryWindow->searchEdit->text().isEmpty()) { + if (!libraryWindow->searchText().isEmpty()) { comicsView->enableFilterMode(true); } } diff --git a/YACReaderLibrary/yacreaderlibrary_de.ts b/YACReaderLibrary/yacreaderlibrary_de.ts index 57c0fdbb0..ae6a71375 100644 --- a/YACReaderLibrary/yacreaderlibrary_de.ts +++ b/YACReaderLibrary/yacreaderlibrary_de.ts @@ -166,24 +166,24 @@ Der aktuelle Theme-JSON konnte nicht geladen werden. - + Import theme Thema importieren - + JSON files (*.json);;All files (*) JSON-Dateien (*.json);;Alle Dateien (*) - + Could not import theme from: %1 Theme konnte nicht importiert werden von: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Der Import ist fehlgeschlagen @@ -293,67 +293,67 @@ ComicModel - + no Nein - + yes Ja - + Read Lesen - + Series Serie - + Volume Volumen - + Story Arc Handlungsbogen - + Size Größe - + Pages Seiten - + Title Titel - + Current Page Aktuelle Seite - + File Name Dateiname - + Publication Date Veröffentlichungsdatum - + Rating Bewertung @@ -617,22 +617,27 @@ FileComic - + Format not supported Format nicht unterstützt - + 7z not found 7z nicht gefunden - + Unknown error opening the file Unbekannter Fehler beim Öffnen der Datei - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly CRC Fehler auf Seite (%1): einige Seiten werden nicht korrekt dargestellt @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Hilfe - + System info Systeminformationen - + + Changelog + Änderungsprotokoll + + + About Über @@ -789,346 +799,415 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>Die aktuelle Bibliothek wird nach alten XML-Metadateninformationen durchsucht.</p><p>Dies ist nur einmal erforderlich und nur, wenn die Bibliothek mit YACReaderLibrary 9.8.2 oder früher erstellt wurde.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>Die aktuelle Bibliothek wird auf fehlende Cover und unvollständige Comic-Informationen überprüft.</p><p>Dies kann mehrere Minuten dauern. Sie können den Vorgang stoppen und später erneut ausführen.</p> + LibraryWindow - + The selected folder doesn't contain any library. Der ausgewählte Ordner enthält keine Bibliothek. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Diese Bibliothek wurde mit einer älteren Version von YACReader erzeugt. Sie muss geupdated werden. Jetzt updaten? - + Comic Komisch - + Error opening the library Fehler beim Öffnen der Bibliothek - - + + YACReader not found YACReader nicht gefunden - Remove and delete metadata - Entferne und lösche Metadaten + Entferne und lösche Metadaten - + Old library Alte Bibliothek - + Set as completed Als gelesen markieren - + Library Bibliothek - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Die Bibliothek wurde mit einer neueren Version von YACReader erstellt. Die neue Version jetzt herunterladen? - + Library '%1' is no longer available. Do you want to remove it? Bibliothek '%1' ist nicht mehr verfügbar. Wollen Sie sie entfernen? - + Open folder... Öffne Ordner... - + Do you want remove Möchten Sie entfernen - + Set as uncompleted Als nicht gelesen markieren - + Error updating the library Fehler beim Updaten der Bibliothek - + Folder Ordner - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? Bibliothek '%1' wurde mit einer älteren Version von YACReader erstellt. Sie muss neu erzeugt werden. Wollen Sie die Bibliothek jetzt erzeugen? - + Set as read Als gelesen markieren - + Library not available Bibliothek nicht verfügbar - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Es gab ein Problem beim Löschen der ausgewählten Comics. Überprüfen Sie bitte die Schreibberechtigung für die ausgewählten Dateien oder Ordner. - + YACReader Library YACReader Bibliothek - + Error creating the library Fehler beim Erstellen der Bibliothek - + Update needed Update benötigt - + Library name already exists Bibliothek-Name bereits vorhanden - + There is another library with the name '%1'. Es gibt bereits eine Bibliothek mit dem Namen '%1'. - + Download new version Neue Version herunterladen - + Delete comics Comics löschen - + All the selected comics will be deleted from your disk. Are you sure? Alle ausgewählten Comics werden von Ihrer Festplatte gelöscht. Sind Sie sicher? - - + + Set as unread Als ungelesen markieren - + Library not found Bibliothek nicht gefunden - - - + + + manga Manga - - - + + + comic komisch - - - + + + web comic Webcomic - - - + + + western manga (left to right) Western-Manga (von links nach rechts) - - + + Unable to delete Löschen nicht möglich - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (von oben nach unten) - + library? Bibliothek? - + Are you sure? Sind Sie sicher? - + Rescan library for XML info Durchsuchen Sie die Bibliothek erneut nach XML-Informationen - + Add new folder Neuen Ordner erstellen - + Delete folder Ordner löschen - + Update folder Ordner aktualisieren - + Upgrade failed Update gescheitert - + There were errors during library upgrade in: Beim Upgrade der Bibliothek kam es zu Fehlern in: - - + + Copying comics... Kopieren von Comics... - - + + Moving comics... Verschieben von Comics... - + Folder name: Ordnername - + No folder selected Kein Ordner ausgewählt - + Please, select a folder first Bitte wählen Sie zuerst einen Ordner aus - + Error in path Fehler im Pfad - + There was an error accessing the folder's path Beim Aufrufen des Ordnerpfades kam es zu einem Fehler - + The selected folder and all its contents will be deleted from your disk. Are you sure? Der ausgewählte Ordner und sein gesamter Inhalt wird von Ihrer Festplatte gelöscht. Sind Sie sicher? - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Beim Löschen des ausgewählten Ordners ist ein Problem aufgetreten. Bitte überprüfen Sie die Schreibrechte und stellen Sie sicher, dass keine Anwendung diese Ordner oder die darin enthaltenen Dateien verwendet. - + Add new reading lists Neue Leseliste hinzufügen - - + + List name: Name der Liste - + Delete list/label Ausgewählte/s Liste/Label löschen - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? Das ausgewählte Element wird gelöscht; Ihre Comics oder Ordner werden NICHT von Ihrer Festplatte gelöscht. Sind Sie sicher? - + Rename list name Listenname ändern - - - - + + + + Set type Typ festlegen - + + Search filters + Suchfilter + + + + Unread + Ungelesen + + + + In progress + In Bearbeitung + + + + Highly rated + Hoch bewertet + + + + Recently added + Kürzlich hinzugefügt + + + + Search syntax… + Suchsyntax… + + + + A repair of this library is already running (%1). Wait for it to finish. + Für diese Bibliothek läuft bereits eine Reparatur (%1). Warten Sie, bis sie abgeschlossen ist. + + + + The library is locked by a repair that did not finish. + Die Bibliothek ist durch eine nicht abgeschlossene Reparatur gesperrt. + + + + The library is locked by a repair started by %1. + Die Bibliothek ist durch eine von %1 gestartete Reparatur gesperrt. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Wenn Sie sicher sind, dass keine andere Reparatur läuft, kann die Sperre entfernt werden. Sperre entfernen und fortfahren? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Wiederherstellung nach Abbruch fehlgeschlagen + + + Set custom cover Legen Sie ein benutzerdefiniertes Cover fest - + Delete custom cover Benutzerdefiniertes Cover löschen - + Save covers Titelbilder speichern - + You are adding too many libraries. Sie fügen zu viele Bibliotheken hinzu. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1141,70 +1220,236 @@ Wahrscheinlich brauchen Sie nur eine Bibliothek in Ihrem obersten Comic-Ordner, YACReaderLibrary wird Sie nicht daran hindern, weitere Bibliotheken zu erstellen, aber Sie sollten die Anzahl der Bibliotheken gering halten. - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader nicht gefunden. YACReader muss im gleichen Ordner installiert sein wie YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader nicht gefunden. Eventuell besteht ein Problem mit Ihrer YACReader-Installation. - + Error Fehler - + Error opening comic with third party reader. Beim Öffnen des Comics mit dem Drittanbieter-Reader ist ein Fehler aufgetreten. - + + + YACReader library database (*.ydb) + YACReader-Bibliotheksdatenbank (*.ydb) + + + + The library database backup was created at: +%1 + Die Sicherung der Bibliotheksdatenbank wurde hier erstellt: +%1 + + + + Unable to create the library database backup: +%1 + Die Sicherung der Bibliotheksdatenbank konnte nicht erstellt werden: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Schließen Sie vor der Wiederherstellung YACReaderLibraryServer und alle anderen YACReader-Anwendungen, die diese Bibliothek verwenden. Fortfahren? + + + + Restoring library database... + Bibliotheksdatenbank wird wiederhergestellt... + + + + The current library database is invalid. Restore the selected backup anyway? + Die aktuelle Bibliotheksdatenbank ist ungültig. Die ausgewählte Sicherung trotzdem wiederherstellen? + + + + + The library maintenance lock may be stale. Remove it and retry? + Die Wartungssperre der Bibliothek ist möglicherweise veraltet. Entfernen und erneut versuchen? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Starten Sie YACReaderLibrary neu, bevor Sie erneut eine Wiederherstellung versuchen. + + + + The library database was restored successfully. Update the library now? + Die Bibliotheksdatenbank wurde erfolgreich wiederhergestellt. Bibliothek jetzt aktualisieren? + + + + Library database damaged + Bibliotheksdatenbank beschädigt + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + Die Datenbank der Bibliothek '%1' ist beschädigt, daher sind normale Aktualisierungen, Wartungsarbeiten und Sicherungen nicht verfügbar. YACReader kann versuchen, die Datenbank zu reparieren. Einige beschädigte Daten können möglicherweise nicht wiederhergestellt werden. Vorhandene Sicherungen werden nicht verändert. + + + + Attempt repair + Reparatur versuchen + + + + Restore a backup... + Sicherung wiederherstellen... + + + + Repairing library database... + Bibliotheksdatenbank wird repariert... + + + + + + Library database repair + Reparatur der Bibliotheksdatenbank + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Ein anderer Wartungsvorgang verwendet diese Bibliothek derzeit. Versuchen Sie es nach dessen Abschluss erneut. + + + + The library database is already valid. + Die Bibliotheksdatenbank ist bereits gültig. + + + + Library database repaired + Bibliotheksdatenbank repariert + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + Die Bibliotheksdatenbank wurde durch den Neuaufbau ihrer Indizes repariert. Das beschädigte Original wurde hier aufbewahrt: +%1 + + + + Library database rebuilt + Bibliotheksdatenbank neu aufgebaut + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + Die Bibliotheksdatenbank wurde erfolgreich neu aufgebaut. Das beschädigte Original wurde hier aufbewahrt: +%1 + +Bibliothek jetzt aktualisieren? + + + + + +The damaged original was preserved at: +%1 + + +Das beschädigte Original wurde hier aufbewahrt: +%1 + + + + Library database repair failed + Reparatur der Bibliotheksdatenbank fehlgeschlagen + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + Die Bibliotheksdatenbank konnte nicht repariert werden: +%1%2 + +Sie können über das Bibliotheksmenü eine Sicherung wiederherstellen oder die Bibliothek neu erstellen. + + + + Remove and delete metadata and backups + Metadaten und Sicherungen entfernen und löschen + + + Library info Informationen zur Bibliothek - + Assign comics numbers Comics Nummern zuweisen - + Assign numbers starting in: Nummern zuweisen, beginnend mit: - + Invalid image Ungültiges Bild - + The selected file is not a valid image. Die ausgewählte Datei ist kein gültiges Bild. - + Error saving cover Fehler beim Speichern des Covers - + There was an error saving the cover image. Beim Speichern des Titelbildes ist ein Fehler aufgetreten. - + Remove comics Comics löschen - + Comics will only be deleted from the current label/list. Are you sure? Comics werden nur vom aktuellen Label/der aktuellen Liste gelöscht. Sind Sie sicher? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Repariert: %1 +Fehlgeschlagen: %2 +Fehlende Dateien: %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary wird Sie nicht daran hindern, weitere Bibliotheken zu erstellen + Back up library database + Bibliotheksdatenbank sichern + + + + Create a backup of the current library database + Eine Sicherung der aktuellen Bibliotheksdatenbank erstellen + + + + Restore library database backup + Sicherung der Bibliotheksdatenbank wiederherstellen + + + + Restore the current library database from a backup + Die aktuelle Bibliotheksdatenbank aus einer Sicherung wiederherstellen + + + + + Repair covers and comic info + Cover und Comic-Informationen reparieren + + + + Retry comics with missing covers or incomplete information + Comics mit fehlenden Covern oder unvollständigen Informationen erneut verarbeiten + + + Rename library Bibliothek umbenennen - + Rename current library Aktuelle Bibliothek umbenennen - + Remove library Bibliothek entfernen - + Remove current library from your collection Aktuelle Bibliothek aus der Sammlung entfernen - + Rescan library for XML info Durchsuchen Sie die Bibliothek erneut nach XML-Informationen - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Versucht, in Comic-Dateien eingebettete XML-Informationen zu finden. Sie müssen dies nur tun, wenn die Bibliothek mit 9.8.2 oder früheren Versionen erstellt wurde oder wenn Sie Software von Drittanbietern verwenden, um XML-Informationen in die Dateien einzubetten. - + + Open library folder... + Bibliotheksordner öffnen... + + + + Open the root folder of the current library + Stammordner der aktuellen Bibliothek öffnen + + + Show library info Bibliotheksinformationen anzeigen - + Show information about the current library Informationen zur aktuellen Bibliothek anzeigen - + Open current comic Aktuellen Comic öffnen - + Open current comic on YACReader Aktuellen Comic mit YACReader öffnen - + Save selected covers to... Ausgewählte Titelbilder speichern in... - + Save covers of the selected comics as JPG files Titelbilder der ausgewählten Comics als JPG-Datei speichern - - + + Set as read Als gelesen markieren - + Set comic as read Comic als gelesen markieren - - + + Set as unread Als ungelesen markieren - + Set comic as unread Comic als ungelesen markieren - - + + manga Manga - + Set issue as manga Ausgabe als Manga festlegen - - + + comic komisch - + Set issue as normal Ausgabe als normal festlegen - + western manga Western-Manga - + Set issue as western manga Ausgabe als Western-Manga festlegen - - + + web comic Webcomic - + Set issue as web comic Ausgabe als Webcomic festlegen - - + + yonkoma Yonkoma - + Set issue as yonkoma Stellen Sie das Problem als Yonkoma ein - + Show/Hide marks Zeige/Verberge Markierungen - + Show or hide read marks Gelesen-Markierungen anzeigen oder verbergen - + Show/Hide recent indicator Aktuelle Anzeige ein-/ausblenden - + Show or hide recent indicator Aktuelle Anzeige anzeigen oder ausblenden - - + + Fullscreen mode on/off Vollbildmodus an/aus - + Help, About YACReader Hilfe, Über YACReader - + Add new folder Neuen Ordner erstellen - + Add new folder to the current library Neuen Ordner in der aktuellen Bibliothek erstellen - + Delete folder Ordner löschen - + Delete current folder from disk Aktuellen Ordner von der Festplatte löschen - + Select root node Ursprungsordner auswählen - + Expand all nodes Alle Unterordner anzeigen - + Collapse all nodes Alle Unterordner einklappen - + Show options dialog Zeige den Optionen-Dialog - + Show comics server options dialog Zeige Comic-Server-Optionen-Dialog - - + + Change between comics views Zwischen Comic-Anzeigemodi wechseln - + Open folder... Öffne Ordner... - + Set as uncompleted Als nicht gelesen markieren - + Set as completed Als gelesen markieren - + Set custom cover Legen Sie ein benutzerdefiniertes Cover fest - + Delete custom cover Benutzerdefiniertes Cover löschen - + western manga (left to right) Western-Manga (von links nach rechts) - + Open containing folder... Öffne aktuellen Ordner... - + Reset comic rating Comic-Bewertung zurücksetzen - + Select all comics Alle Comics auswählen - + Edit Bearbeiten - + Assign current order to comics Aktuele Sortierung auf Comics anwenden - + Update cover Titelbild updaten - + Delete selected comics Ausgewählte Comics löschen - + Delete metadata from selected comics Metadaten aus ausgewählten Comics löschen - + Download tags from Comic Vine Tags von Comic Vine herunterladen - + Focus search line Suchzeile fokussieren - + Focus comics view Fokus-Comic-Ansicht - + Edit shortcuts Kürzel bearbeiten - + &Quit &Schließen - + Update folder Ordner aktualisieren - + Update current folder Aktuellen Ordner aktualisieren - + Scan legacy XML metadata Scannen Sie ältere XML-Metadaten - + Add new reading list Neue Leseliste hinzufügen - + Add a new reading list to the current library Neue Leseliste zur aktuellen Bibliothek hinzufügen - + Remove reading list Leseliste entfernen - + Remove current reading list from the library Aktuelle Leseliste von der Bibliothek entfernen - + Add new label Neues Label hinzufügen - + Add a new label to this library Neues Label zu dieser Bibliothek hinzufügen - + Rename selected list Ausgewählte Liste umbenennen - + Rename any selected labels or lists Ausgewählte Labels oder Listen umbenennen - + Add to... Hinzufügen zu... - + Favorites Favoriten - + Add selected comics to favorites list Ausgewählte Comics zu Favoriten hinzufügen @@ -1691,78 +1977,78 @@ YACReaderLibrary wird Sie nicht daran hindern, weitere Bibliotheken zu erstellen Aussehen - + Options Optionen - + Language Sprache - + Application language Anwendungssprache - + System default Systemstandard - + Tray icon settings (experimental) Taskleisten-Einstellungen (experimentell) - + Close to tray In Taskleiste schließen - + Start into the system tray In die Taskleiste starten - + Edit Comic Vine API key Comic Vine API-Schlüssel ändern - + Comic Vine API key Comic Vine API Schlüssel - + ComicInfo.xml legacy support ComicInfo.xml-Legacy-Unterstützung - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importieren Sie Metadaten aus ComicInfo.xml, wenn Sie neue Comics hinzufügen - + Consider 'recent' items added or updated since X days ago Berücksichtigen Sie „neue“ Elemente, die seit X Tagen hinzugefügt oder aktualisiert wurden - + Third party reader Drittanbieter-Reader - + Write {comic_file_path} where the path should go in the command Schreiben Sie {comic_file_path}, wohin der Pfad im Befehl gehen soll - + Clear Löschen @@ -1922,7 +2208,7 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n Allgemein - + Restart is needed Neustart erforderlich @@ -2292,6 +2578,279 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n Anzahl der gefundenen Bände: %1 + + SearchFieldRegistry + + + Text, quoted text + Text, Text in Anführungszeichen + + + + Integer + Ganzzahl + + + + Boolean (true / false) + Boolescher Wert (true / false) + + + + Integer (number of days) + Ganzzahl (Anzahl der Tage) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Aufzählung (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Comictitel + + + + Series name + Serienname + + + + Issue number + Ausgabennummer + + + + Volume identifier + Bandkennung + + + + Reading format + Leseformat + + + + Comic rating + Comicbewertung + + + + Textual tags + Text-Tags + + + + Writer credit + Autor + + + + Penciller credit + Zeichner + + + + Inker credit + Tuscher + + + + Colorist credit + Kolorist + + + + Letterer credit + Letterer + + + + Cover artist credit + Coverzeichner + + + + Editor credit + Redakteur + + + + Story arc name + Name des Handlungsbogens + + + + Position within a story arc + Position innerhalb eines Handlungsbogens + + + + Number of issues in a story arc + Anzahl der Ausgaben in einem Handlungsbogen + + + + Characters appearing in the comic + Im Comic vorkommende Figuren + + + + Teams appearing in the comic + Im Comic vorkommende Teams + + + + Locations appearing in the comic + Im Comic vorkommende Orte + + + + Primary character or team + Hauptfigur oder Hauptteam + + + + Comic synopsis + Comic-Zusammenfassung + + + + Publisher name + Name des Verlags + + + + Publishing imprint + Verlagsimprint + + + + Publication format + Veröffentlichungsformat + + + + Recommended age rating + Empfohlene Altersfreigabe + + + + Comic genre + Comicgenre + + + + ISO language code + ISO-Sprachcode + + + + Publication date metadata + Metadaten zum Veröffentlichungsdatum + + + + Series grouping metadata + Metadaten zur Seriengruppierung + + + + Alternate series name + Alternativer Serienname + + + + Alternate issue number + Alternative Ausgabennummer + + + + Alternate series issue count + Anzahl der Ausgaben der alternativen Serie + + + + Number of issues in the series + Anzahl der Ausgaben in der Serie + + + + Whether the comic is marked as read + Ob der Comic als gelesen markiert ist + + + + Whether reading has started + Ob mit dem Lesen begonnen wurde + + + + Whether metadata has been edited + Ob die Metadaten bearbeitet wurden + + + + Whether the comic is in color + Ob der Comic farbig ist + + + + Number of pages + Seitenzahl + + + + Comic file name + Dateiname des Comics + + + + When the item was added + Wann das Element hinzugefügt wurde + + + + When the comic was last opened + Wann der Comic zuletzt geöffnet wurde + + + + Comic notes + Comicnotizen + + + + Review text + Rezensionstext + + + + Parent folder name + Name des übergeordneten Ordners + + + + Default reading format for the folder + Standard-Leseformat des Ordners + + + + Whether the folder is complete + Ob der Ordner vollständig ist + + + + Whether the folder is marked as finished + Ob der Ordner als beendet markiert ist + + + + When the folder was updated + Wann der Ordner aktualisiert wurde + + SearchSingleComic @@ -2311,6 +2870,301 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n Verwenden Sie die Suche nach exakten Übereinstimmungen. Deaktivieren Sie diese Option, wenn Sie Bände finden möchten, die mit einigen Wörtern im Namen übereinstimmen. + + SearchSyntaxDialog + + + Common + Allgemein + + + + Credits + Mitwirkende + + + + Story + Handlung + + + + Publication + Veröffentlichung + + + + Reading & files + Lesen und Dateien + + + + Folders + Ordner + + + + Search syntax + Suchsyntax + + + + Search every comic and folder field, or build precise queries. + Durchsuche alle Comic- und Ordnerfelder oder erstelle präzise Abfragen. + + + + Quick guide + Kurzanleitung + + + + Fields (%1) + Felder (%1) + + + + Examples + Beispiele + + + + Start with a simple search + Mit einer einfachen Suche beginnen + + + + Just start typing. Plain text search across all metadata. + Beginne einfach mit der Eingabe. Einfacher Text durchsucht alle Metadaten. + + + + 1. Search everywhere + 1. Überall suchen + + + + Type any text or quoted text. + Gib beliebigen Text oder Text in Anführungszeichen ein. + + + + 2. Target a field + 2. Ein Feld auswählen + + + + Use a field name followed by : or = + Verwende einen Feldnamen gefolgt von : oder = + + + + 3. Combine conditions + 3. Bedingungen kombinieren + + + + Use AND, OR, NOT and parentheses. + Verwende AND, OR, NOT und Klammern. + + + + Operators + Operatoren + + + + : or = + : oder = + + + + contains the text + enthält den Text + + + + matches the complete value + entspricht dem vollständigen Wert + + + + greater than / at least + größer als / mindestens + + + + less than / at most + kleiner als / höchstens + + + + "quoted text" + "Text in Anführungszeichen" + + + + keeps spaces inside one value + behält Leerzeichen innerhalb eines Werts bei + + + + Dates and grouping + Datumsangaben und Gruppierung + + + + added in the last 7 days + in den letzten 7 Tagen hinzugefügt + + + + added more than 30 days ago + vor mehr als 30 Tagen hinzugefügt + + + + group alternatives + gruppiert Alternativen + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Tipps: Leerzeichen wirken wie AND und die Suche unterscheidet nicht zwischen Groß- und Kleinschreibung. Verwende Anführungszeichen für Leerzeichen in einem Wert. + + + + Find a field… + Feld suchen… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Text kann direkt oder in Anführungszeichen eingegeben werden. Ganzzahlfelder unterstützen <, <=, > und >=. Bei Datumsfeldern steht die Ganzzahl für die Anzahl der Tage (added>7 bedeutet: in den letzten 7 Tagen hinzugefügt). + + + + Field + Feld + + + + Description + Beschreibung + + + + Input + Eingabe + + + + Example + Beispiel + + + + Examples show the pattern—replace the values with your own. + Die Beispiele zeigen das Muster — ersetze die Werte durch deine eigenen. + + + + Query + Abfrage + + + + What it finds + Was gefunden wird + + + + Common filters + Häufige Filter + + + + Unread comics + Ungelesene Comics + + + + Comics in progress + Begonnene Comics + + + + Highly rated comics + Hoch bewertete Comics + + + + Comics added in the last 7 days + In den letzten 7 Tagen hinzugefügte Comics + + + + Metadata + Metadaten + + + + Search by series + Nach Serie suchen + + + + Search by writer + Nach Autor suchen + + + + Manga comics + Manga-Comics + + + + Search textual tags + Text-Tags durchsuchen + + + + Advanced combinations + Erweiterte Kombinationen + + + + Match either writer + Mit einem der Autoren übereinstimmen + + + + Group alternatives + Alternativen gruppieren + + + + Exclude a value + Einen Wert ausschließen + + + + Older, highly rated comics + Ältere, hoch bewertete Comics + + + + Copy query + Abfrage kopieren + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Leerzeichen wirken wie AND. Verwende Anführungszeichen für Ausdrücke und Klammern, um die Gruppierung zu steuern. + + SearchVolume @@ -2416,39 +3270,81 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n ServerConfigDialog - + + + Server connectivity + Serverkonnektivität + + + + Scan to connect + Zum Verbinden scannen + + + + Devices on this network can reach your library at the address below. + Geräte in diesem Netzwerk können Ihre Bibliothek unter der unten angegebenen Adresse erreichen. + + + + IP address + IP-Adresse + + + Port Anschluss - - enable the server + + Web interface + Weboberfläche + + + + Copy link + Link kopieren + + + + Open web UI + Weboberfläche öffnen + + + + Enable the server Server aktivieren - - set port - Anschluss wählen + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader ist für iOS und Android verfügbar. Entdecken Sie es für <a href='https://ios.yacreader.com'>iOS</a> oder <a href='https://android.yacreader.com'>Android</a>. + + + enable the server + Server aktivieren + + + + Set port + set port + Port festlegen - Server connectivity information - Serveranschluss-Information + Serveranschluss-Information - Scan it! - Durchsuchen! + Durchsuchen! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader ist für iOS- und Android-Geräte verfügbar.<br/>Entdecken Sie es für <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> oder <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader ist für iOS- und Android-Geräte verfügbar.<br/>Entdecken Sie es für <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> oder <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - IP-Adresse auswählen + IP-Adresse auswählen @@ -2867,14 +3763,14 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n YACReader::WhatsNewDialog - + Release notes are not available. - + Versionshinweise sind nicht verfügbar. - + Previous versions - + Frühere Versionen @@ -2910,22 +3806,32 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n YACReaderOptionsDialog - + Save Speichern - + Cancel Abbrechen - + + Keyboard shortcuts + Tastenkürzel + + + + Customize the keyboard shortcuts used by the application. + Passen Sie die von der Anwendung verwendeten Tastenkürzel an. + + + Edit shortcuts Kürzel bearbeiten - + Shortcuts Kürzel @@ -2933,7 +3839,12 @@ Um eine automatische Aktualisierung zu stoppen, tippen Sie auf die Ladeanzeige n YACReaderSearchLineEdit - + + Search filters + Suchfilter + + + type to search tippen, um zu suchen diff --git a/YACReaderLibrary/yacreaderlibrary_en.ts b/YACReaderLibrary/yacreaderlibrary_en.ts index 6d47872b5..fe6cac159 100644 --- a/YACReaderLibrary/yacreaderlibrary_en.ts +++ b/YACReaderLibrary/yacreaderlibrary_en.ts @@ -34,35 +34,39 @@ AddLibraryDialog - - - Comics folder : - Comics folder : - - - - Library name : - Library Name : - Library name : - Add Add + + + Add an existing library + Add an existing library + Cancel Cancel - - Add an existing library - Add an existing library + + Comics folder : + Comics folder : + + + + Library name : + Library name : ApiKeyDialog + + + Cancel + Cancel + Before you can connect to Comic Vine, you need your own API key. Please, get one free <a href="http://www.comicvine.com/api/">here</a> @@ -78,11 +82,6 @@ Accept Accept - - - Cancel - Cancel - AppearanceTabWidget @@ -167,24 +166,24 @@ The current theme JSON could not be loaded. - + Import theme Import theme - + JSON files (*.json);;All files (*) JSON files (*.json);;All files (*) - + Could not import theme from: %1 Could not import theme from: %1 - + Could not import theme from: %1 @@ -195,7 +194,7 @@ %2 - + Import failed Import failed @@ -213,148 +212,148 @@ Characters - + Characters Main character or team - + Main character or team Teams - + Teams Locations - + Locations Authors - Authors + Authors writer - + writer penciller - + penciller inker - + inker colorist - + colorist letterer - + letterer cover artist - + cover artist editor - + editor imprint - + imprint Publisher - + Publisher color - + color b/w - + b/w ComicModel - + yes yes - + no no - + Title Title - + File Name File Name - + Pages Pages - + Size Size - + Read Read - + Current Page Current Page - + Publication Date Publication Date - + Rating Rating - + Series Series - + Volume Volume - + Story Arc Story Arc @@ -429,14 +428,14 @@ CreateLibraryDialog - - Comics folder : - Comics folder : + + Create new library + Create new library - - Library Name : - Library Name : + + Cancel + Cancel @@ -444,20 +443,20 @@ Create - - Cancel - Cancel + + Comics folder : + Comics folder : + + + + Library Name : + Library Name : Create a library could take several minutes. You can stop the process and update the library later for completing the task. Create a library could take several minutes. You can stop the process and update the library later for completing the task. - - - Create new library - Create new library - Path not found @@ -518,7 +517,6 @@ This reading list does not contain any comics yet - This reading list doesn't contain any comics yet This reading list does not contain any comics yet @@ -543,9 +541,9 @@ ExportComicsInfoDialog - - Output file : - Output file : + + Cancel + Cancel @@ -553,9 +551,9 @@ Create - - Cancel - Cancel + + Output file : + Output file : @@ -581,9 +579,9 @@ ExportLibraryDialog - - Output folder : - Output folder : + + Cancel + Cancel @@ -591,15 +589,20 @@ Create - - Cancel - Cancel + + Output folder : + Output folder : Create covers package Create covers package + + + Destination directory + Destination directory + Problem found while writing @@ -610,41 +613,41 @@ The selected path for the output file does not exist or is not a valid path. Be sure that you have write access to this folder The selected path for the output file does not exist or is not a valid path. Be sure that you have write access to this folder - - - Destination directory - Destination directory - FileComic - + + 7z not found + 7z not found + + + CRC error on page (%1): some of the pages will not be displayed correctly CRC error on page (%1): some of the pages will not be displayed correctly - + Unknown error opening the file Unknown error opening the file - - 7z not found - 7z not found - - - + Format not supported Format not supported + + + Unsupported EPUB: %1 + + FolderContentView Continue Reading... - + Continue Reading... @@ -658,23 +661,33 @@ HelpAboutDialog - + About About - + Help Help - + System info System info + + + Changelog + Changelog + ImportComicsInfoDialog + + + Cancel + Cancel + Import comics info @@ -690,11 +703,6 @@ Import Import - - - Cancel - Cancel - Comics info file (*.ydb) @@ -703,41 +711,41 @@ ImportLibraryDialog - - - Library Name : - Library Name : - - - - Package location : - Package location : - Destination folder : Destination folder : - - - Unpack - Unpack - Cancel Cancel - - Extract a catalog - Extract a catalog + + Unpack + Unpack Compresed library covers (*.clc) Compresed library covers (*.clc) + + + Package location : + Package location : + + + + Library Name : + Library Name : + + + + Extract a catalog + Extract a catalog + ImportWidget @@ -791,259 +799,350 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + LibraryWindow - - YACReader Library - YACReader Library - - - + Library Library - - Set as read - Set as read + + Open folder... + Open folder... - - - Set as unread - Set as unread + + + + western manga (left to right) + western manga (left to right) + + + + + + 4koma (top to botom) + 4koma (top to botom + 4koma (top to botom) + + + + Do you want remove + Do you want remove + + + + YACReader Library + YACReader Library - - - + + + manga manga - - - + + + comic comic - - - - web comic - web comic + + Are you sure? + Are you sure? - - - - western manga (left to right) - western manga (left to right) + + Rescan library for XML info + Rescan library for XML info - - Library not available - Library ' - Library not available + + Set as read + Set as read - - Rescan library for XML info - Rescan library for XML info + + + Set as unread + Set as unread - - Delete folder - Delete folder + + + + web comic + web comic - - Open folder... - Open folder... + + Add new folder + Add new folder + + + + Delete folder + Delete folder - + Set as uncompleted Set as uncompleted - + Set as completed Set as completed - + Update folder Update folder - + Folder Folder - + Comic Comic - + Upgrade failed Upgrade failed - + There were errors during library upgrade in: There were errors during library upgrade in: - + + Restore recovery failed + Restore recovery failed + + + Update needed Update needed - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? - + Download new version Download new version - + This library was created with a newer version of YACReaderLibrary. Download the new version now? This library was created with a newer version of YACReaderLibrary. Download the new version now? - + + Library not available + Library not available + + + Library '%1' is no longer available. Do you want to remove it? Library '%1' is no longer available. Do you want to remove it? - + Old library Old library - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? - - + + Copying comics... Copying comics... - - + + Moving comics... Moving comics... - + Folder name: Folder name: - + No folder selected No folder selected - + Please, select a folder first Please, select a folder first - + Error in path Error in path - + There was an error accessing the folder's path There was an error accessing the folder's path - + The selected folder and all its contents will be deleted from your disk. Are you sure? The selected folder and all its contents will be deleted from your disk. Are you sure? - - There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. - There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. - + + + Unable to delete + Unable to delete + + + + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. + - + Add new reading lists Add new reading lists - - + + List name: List name: - + Delete list/label Delete list/label - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? - + Rename list name Rename list name - - - - 4koma (top to botom) - 4koma (top to botom) - - - - - - + + + + Set type Set type - + + Search filters + Search filters + + + + Unread + Unread + + + + In progress + In progress + + + + Highly rated + Highly rated + + + + Recently added + Recently added + + + + Search syntax… + Search syntax… + + + + A repair of this library is already running (%1). Wait for it to finish. + A repair of this library is already running (%1). Wait for it to finish. + + + + The library is locked by a repair that did not finish. + The library is locked by a repair that did not finish. + + + + The library is locked by a repair started by %1. + The library is locked by a repair started by %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Set custom cover Set custom cover - + Delete custom cover Delete custom cover - + Save covers Save covers - + You are adding too many libraries. You are adding too many libraries. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1056,157 +1155,297 @@ You probably only need one library in your top level comics folder, you can brow YACReaderLibrary will not stop you from creating more libraries but you should keep the number of libraries low. - - + + YACReader not found YACReader not found - + + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. + + + + YACReader not found. There might be a problem with your YACReader installation. + YACReader not found. There might be a problem with your YACReader installation. + + + Error Error - + Error opening comic with third party reader. Error opening comic with third party reader. - + Library not found Library not found - + The selected folder doesn't contain any library. The selected folder doesn't contain any library. - - Are you sure? - Are you sure? + + + YACReader library database (*.ydb) + YACReader library database (*.ydb) - - Do you want remove - Do you want remove + + The library database backup was created at: +%1 + The library database backup was created at: +%1 - - library? - library? + + Unable to create the library database backup: +%1 + Unable to create the library database backup: +%1 - - Remove and delete metadata - Remove and delete metadata + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? - - Library info - Library info + + Restoring library database... + Restoring library database... - - Assign comics numbers - Assign comics numbers + + The current library database is invalid. Restore the selected backup anyway? + The current library database is invalid. Restore the selected backup anyway? - - Assign numbers starting in: - Assign numbers starting in: + + + The library maintenance lock may be stale. Remove it and retry? + The library maintenance lock may be stale. Remove it and retry? - - - Unable to delete - Unable to delete + + + +Restart YACReaderLibrary before attempting recovery again. + + +Restart YACReaderLibrary before attempting recovery again. - - Add new folder - Add new folder + + The library database was restored successfully. Update the library now? + The library database was restored successfully. Update the library now? - - YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. - YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. + + Library database damaged + Library database damaged - - YACReader not found. There might be a problem with your YACReader installation. - YACReader not found. There might be a problem with your YACReader installation. + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + + + + Attempt repair + Attempt repair + + + + Restore a backup... + Restore a backup... + + + + Repairing library database... + Repairing library database... + + + + + + Library database repair + Library database repair + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Another maintenance operation is currently using this library. Try again after it finishes. + + + + The library database is already valid. + The library database is already valid. + + + + Library database repaired + Library database repaired + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + + + + Library database rebuilt + Library database rebuilt + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + + + + + +The damaged original was preserved at: +%1 + + +The damaged original was preserved at: +%1 + + + + Library database repair failed + Library database repair failed + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + + + + library? + library? + + + + Remove and delete metadata and backups + Remove and delete metadata and backups + + + + Library info + Library info - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. - + + Assign comics numbers + Assign comics numbers + + + + Assign numbers starting in: + Assign numbers starting in: + + + Invalid image Invalid image - + The selected file is not a valid image. The selected file is not a valid image. - + Error saving cover Error saving cover - + There was an error saving the cover image. There was an error saving the cover image. - + Error creating the library Error creating the library - + Error updating the library Error updating the library - + Error opening the library Error opening the library - + Delete comics Delete comics - + All the selected comics will be deleted from your disk. Are you sure? All the selected comics will be deleted from your disk. Are you sure? - + Remove comics Remove comics - + Comics will only be deleted from the current label/list. Are you sure? Comics will only be deleted from the current label/list. Are you sure? - + Library name already exists Library name already exists - + There is another library with the name '%1'. There is another library with the name '%1'. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Repaired: %1 +Failed: %2 +Missing files: %3 + LibraryWindowActions @@ -1264,384 +1503,425 @@ YACReaderLibrary will not stop you from creating more libraries but you should k + Back up library database + Back up library database + + + + Create a backup of the current library database + Create a backup of the current library database + + + + Restore library database backup + Restore library database backup + + + + Restore the current library database from a backup + Restore the current library database from a backup + + + + + Repair covers and comic info + Repair covers and comic info + + + + Retry comics with missing covers or incomplete information + Retry comics with missing covers or incomplete information + + + Rename library Rename library - + Rename current library Rename current library - + Remove library Remove library - + Remove current library from your collection Remove current library from your collection - + Rescan library for XML info Rescan library for XML info - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. - + + Open library folder... + Open library folder... + + + + Open the root folder of the current library + Open the root folder of the current library + + + Show library info Show library info - + Show information about the current library Show information about the current library - + Open current comic Open current comic - + Open current comic on YACReader Open current comic on YACReader - + Save selected covers to... Save selected covers to... - + Save covers of the selected comics as JPG files Save covers of the selected comics as JPG files - - + + Set as read Set as read - + Set comic as read Set comic as read - - + + Set as unread Set as unread - + Set comic as unread Set comic as unread - - + + manga manga - + Set issue as manga Set issue as manga - - + + comic comic - + Set issue as normal Set issue as normal - + western manga western manga - + Set issue as western manga Set issue as western manga - - + + web comic web comic - + Set issue as web comic Set issue as web comic - - + + yonkoma yonkoma - + Set issue as yonkoma Set issue as yonkoma - + Show/Hide marks Show/Hide marks - + Show or hide read marks Show or hide read marks - + Show/Hide recent indicator Show/Hide recent indicator - + Show or hide recent indicator Show or hide recent indicator - - + + Fullscreen mode on/off Fullscreen mode on/off - + Help, About YACReader Help, About YACReader - + Add new folder Add new folder - + Add new folder to the current library Add new folder to the current library - + Delete folder Delete folder - + Delete current folder from disk Delete current folder from disk - + Select root node Select root node - + Expand all nodes Expand all nodes - + Collapse all nodes Collapse all nodes - + Show options dialog Show options dialog - + Show comics server options dialog Show comics server options dialog - - + + Change between comics views Change between comics views - + Open folder... Open folder... - + Set as uncompleted Set as uncompleted - + Set as completed Set as completed - + Set custom cover Set custom cover - + Delete custom cover Delete custom cover - + western manga (left to right) western manga (left to right) - + Open containing folder... Open containing folder... - + Reset comic rating Reset comic rating - + Select all comics Select all comics - + Edit Edit - + Assign current order to comics Assign current order to comics - + Update cover Update cover - + Delete selected comics Delete selected comics - + Delete metadata from selected comics Delete metadata from selected comics - + Download tags from Comic Vine Download tags from Comic Vine - + Focus search line Focus search line - + Focus comics view Focus comics view - + Edit shortcuts Edit shortcuts - + &Quit &Quit - + Update folder Update folder - + Update current folder Update current folder - + Scan legacy XML metadata Scan legacy XML metadata - + Add new reading list Add new reading list - + Add a new reading list to the current library Add a new reading list to the current library - + Remove reading list Remove reading list - + Remove current reading list from the library Remove current reading list from the library - + Add new label Add new label - + Add a new label to this library Add a new label to this library - + Rename selected list Rename selected list - + Rename any selected labels or lists Rename any selected labels or lists - + Add to... Add to... - + Favorites Favorites - + Add selected comics to favorites list Add selected comics to favorites list @@ -1688,73 +1968,73 @@ YACReaderLibrary will not stop you from creating more libraries but you should k OptionsDialog - + Language - + Language - + Application language - + Application language - + System default - + System default - + Tray icon settings (experimental) Tray icon settings (experimental) - + Close to tray Close to tray - + Start into the system tray Start into the system tray - + Edit Comic Vine API key Edit Comic Vine API key - + Comic Vine API key Comic Vine API key - + ComicInfo.xml legacy support ComicInfo.xml legacy support - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Import metadata from ComicInfo.xml when adding new comics - + Consider 'recent' items added or updated since X days ago Consider 'recent' items added or updated since X days ago - + Third party reader Third party reader - + Write {comic_file_path} where the path should go in the command Write {comic_file_path} where the path should go in the command - + Clear Clear @@ -1919,12 +2199,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Appearance - + Options Options - + Restart is needed Restart is needed @@ -2036,7 +2316,6 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Genre: - Genere: Genre: @@ -2256,14 +2535,9 @@ To stop an automatic update tap on the loading indicator next to the Libraries t RenameLibraryDialog - - New Library Name : - New Library Name : - - - - Rename - Rename + + Rename current library + Rename current library @@ -2271,12 +2545,17 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Cancel - - Rename current library - Rename current library + + Rename + Rename - - + + + New Library Name : + New Library Name : + + + ScraperResultsPaginator @@ -2295,6 +2574,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Number of %1 found : %2 + + SearchFieldRegistry + + + Text, quoted text + Text, quoted text + + + + Integer + Integer + + + + Boolean (true / false) + Boolean (true / false) + + + + Integer (number of days) + Integer (number of days) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Comic title + + + + Series name + Series name + + + + Issue number + Issue number + + + + Volume identifier + Volume identifier + + + + Reading format + Reading format + + + + Comic rating + Comic rating + + + + Textual tags + Textual tags + + + + Writer credit + Writer credit + + + + Penciller credit + Penciller credit + + + + Inker credit + Inker credit + + + + Colorist credit + Colorist credit + + + + Letterer credit + Letterer credit + + + + Cover artist credit + Cover artist credit + + + + Editor credit + Editor credit + + + + Story arc name + Story arc name + + + + Position within a story arc + Position within a story arc + + + + Number of issues in a story arc + Number of issues in a story arc + + + + Characters appearing in the comic + Characters appearing in the comic + + + + Teams appearing in the comic + Teams appearing in the comic + + + + Locations appearing in the comic + Locations appearing in the comic + + + + Primary character or team + Primary character or team + + + + Comic synopsis + Comic synopsis + + + + Publisher name + Publisher name + + + + Publishing imprint + Publishing imprint + + + + Publication format + Publication format + + + + Recommended age rating + Recommended age rating + + + + Comic genre + Comic genre + + + + ISO language code + ISO language code + + + + Publication date metadata + Publication date metadata + + + + Series grouping metadata + Series grouping metadata + + + + Alternate series name + Alternate series name + + + + Alternate issue number + Alternate issue number + + + + Alternate series issue count + Alternate series issue count + + + + Number of issues in the series + Number of issues in the series + + + + Whether the comic is marked as read + Whether the comic is marked as read + + + + Whether reading has started + Whether reading has started + + + + Whether metadata has been edited + Whether metadata has been edited + + + + Whether the comic is in color + Whether the comic is in color + + + + Number of pages + Number of pages + + + + Comic file name + Comic file name + + + + When the item was added + When the item was added + + + + When the comic was last opened + When the comic was last opened + + + + Comic notes + Comic notes + + + + Review text + Review text + + + + Parent folder name + Parent folder name + + + + Default reading format for the folder + Default reading format for the folder + + + + Whether the folder is complete + Whether the folder is complete + + + + Whether the folder is marked as finished + Whether the folder is marked as finished + + + + When the folder was updated + When the folder was updated + + SearchSingleComic @@ -2314,6 +2866,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Use exact match search. Disable if you want to find volumes that match some of the words in the name. + + SearchSyntaxDialog + + + Common + Common + + + + Credits + Credits + + + + Story + Story + + + + Publication + Publication + + + + Reading & files + Reading & files + + + + Folders + Folders + + + + Search syntax + Search syntax + + + + Search every comic and folder field, or build precise queries. + Search every comic and folder field, or build precise queries. + + + + Quick guide + Quick guide + + + + Fields (%1) + Fields (%1) + + + + Examples + Examples + + + + Start with a simple search + Start with a simple search + + + + Just start typing. Plain text search across all metadata. + Just start typing. Plain text search across all metadata. + + + + 1. Search everywhere + 1. Search everywhere + + + + Type any text or quoted text. + Type any text or quoted text. + + + + 2. Target a field + 2. Target a field + + + + Use a field name followed by : or = + Use a field name followed by : or = + + + + 3. Combine conditions + 3. Combine conditions + + + + Use AND, OR, NOT and parentheses. + Use AND, OR, NOT and parentheses. + + + + Operators + Operators + + + + : or = + : or = + + + + contains the text + contains the text + + + + matches the complete value + matches the complete value + + + + greater than / at least + greater than / at least + + + + less than / at most + less than / at most + + + + "quoted text" + "quoted text" + + + + keeps spaces inside one value + keeps spaces inside one value + + + + Dates and grouping + Dates and grouping + + + + added in the last 7 days + added in the last 7 days + + + + added more than 30 days ago + added more than 30 days ago + + + + group alternatives + group alternatives + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Tips: Spaces act like AND, and searches are not case-sensitive. Use quotes to include spaces in a value. + + + + Find a field… + Find a field… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + + + + Field + Field + + + + Description + Description + + + + Input + Input + + + + Example + Example + + + + Examples show the pattern—replace the values with your own. + Examples show the pattern—replace the values with your own. + + + + Query + Query + + + + What it finds + What it finds + + + + Common filters + Common filters + + + + Unread comics + Unread comics + + + + Comics in progress + Comics in progress + + + + Highly rated comics + Highly rated comics + + + + Comics added in the last 7 days + Comics added in the last 7 days + + + + Metadata + Metadata + + + + Search by series + Search by series + + + + Search by writer + Search by writer + + + + Manga comics + Manga comics + + + + Search textual tags + Search textual tags + + + + Advanced combinations + Advanced combinations + + + + Match either writer + Match either writer + + + + Group alternatives + Group alternatives + + + + Exclude a value + Exclude a value + + + + Older, highly rated comics + Older, highly rated comics + + + + Copy query + Copy query + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + + SearchVolume @@ -2419,39 +3266,81 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - - set port - set port + + Set port + set port + Set port - Server connectivity information - Server connectivity information + Server connectivity information - Scan it! - Scan it! + Scan it! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Choose an IP address + Choose an IP address + + + + + Server connectivity + Server connectivity - + + Scan to connect + Scan to connect + + + + Devices on this network can reach your library at the address below. + Devices on this network can reach your library at the address below. + + + + IP address + IP address + + + Port Port - + + Web interface + Web interface + + + + Copy link + Copy link + + + + Open web UI + Open web UI + + + + Enable the server + Enable the server + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + + enable the server - enable the server + enable the server @@ -2689,16 +3578,16 @@ To stop an automatic update tap on the loading indicator next to the Libraries t UpdateLibraryDialog - - - Updating.... - Updating.... - Cancel Cancel + + + Updating.... + Updating.... + Update library @@ -2870,14 +3759,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + Release notes are not available. - + Previous versions - + Previous versions @@ -2913,22 +3802,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save Save - + Cancel Cancel - + + Keyboard shortcuts + Keyboard shortcuts + + + + Customize the keyboard shortcuts used by the application. + Customize the keyboard shortcuts used by the application. + + + Edit shortcuts Edit shortcuts - + Shortcuts Shortcuts @@ -2936,7 +3835,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + Search filters + + + type to search type to search diff --git a/YACReaderLibrary/yacreaderlibrary_es.ts b/YACReaderLibrary/yacreaderlibrary_es.ts index d26e4c4ef..a0741405e 100644 --- a/YACReaderLibrary/yacreaderlibrary_es.ts +++ b/YACReaderLibrary/yacreaderlibrary_es.ts @@ -166,24 +166,24 @@ No se ha podido cargar el JSON del tema actual. - + Import theme Importar tema - + JSON files (*.json);;All files (*) Archivos JSON (*.json);;Todos los archivos (*) - + Could not import theme from: %1 No se pudo importar el tema desde: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Error al importar @@ -293,67 +293,67 @@ ComicModel - + no No - + yes - + Read Leído - + Series Serie - + Volume Volumen - + Story Arc Arco argumental - + Size Tamaño - + Pages Páginas - + Title Título - + Current Page Página Actual - + File Name Nombre de archivo - + Publication Date Fecha de publicación - + Rating Nota @@ -617,22 +617,27 @@ FileComic - + Format not supported Formato no soportado - + 7z not found 7z no encontrado - + Unknown error opening the file Error desconocido abriendo el archivo - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Error CRC en la página (%1): algunas de las páginas no se mostrarán correctamente @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Ayuda - + System info Información de systema - + + Changelog + Registro de cambios + + + About Acerca de @@ -789,346 +799,415 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>La biblioteca está siendo escaneada para encontrar metadatos en formato XML.</p><p>Sólo necesitas hacer esto una vez, y sólo si la biblioteca fue creada con YACReaderLibrary 9.8.2 o antes.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>Se está comprobando si faltan portadas o información de cómics incompleta en la biblioteca actual.</p><p>Esto puede tardar varios minutos. Puedes detener el proceso y volver a ejecutarlo más tarde.</p> + LibraryWindow - + The selected folder doesn't contain any library. La carpeta seleccionada no contiene ninguna biblioteca. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Esta biblioteca fue creada con una versión anterior de YACReaderLibrary. Es necesario que se actualice. ¿Deseas hacerlo ahora? - + Comic Cómic - + Error opening the library Error abriendo la biblioteca - - + + YACReader not found YACReader no encontrado - Remove and delete metadata - Eliminar y borrar metadatos + Eliminar y borrar metadatos - + Old library Biblioteca antigua - + Set as completed Marcar como completo - + Library Librería - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Esta biblioteca fue creada con una versión más nueva de YACReaderLibrary. ¿Deseas descargar la nueva versión ahora? - + Library '%1' is no longer available. Do you want to remove it? La biblioteca '%1' no está disponible. ¿Deseas eliminarla? - + Open folder... Abrir carpeta... - + Do you want remove ¿Deseas eliminar la biblioteca - + Set as uncompleted Marcar como incompleto - + Error updating the library Error actualizando la biblioteca - + Folder Carpeta - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? La biblioteca '%1' ha sido creada con una versión más antigua de YACReaderLibrary y debe ser creada de nuevo. ¿Deseas crear la biblioteca ahora? - + Set as read Marcar como leído - + Library not available Biblioteca no disponible - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Ha habido algún problema intentando borrar los cómics selecionados. Por favor, verifica los permisos de escritura en los arhicovs seleccionados o los directorios que los conienen. - + YACReader Library Biblioteca YACReader - + Error creating the library Errar creando la biblioteca - + Update needed Se necesita actualizar - + Library name already exists Ya existe el nombre de la biblioteca - + There is another library with the name '%1'. Hay otra biblioteca con el nombre '%1'. - + Download new version Descargar la nueva versión - + Delete comics Borrar cómics - + All the selected comics will be deleted from your disk. Are you sure? Todos los cómics seleccionados serán borrados de tu disco. ¿Estás seguro? - - + + Set as unread Marcar como no leído - + Library not found Biblioteca no encontrada - - - + + + manga historieta manga - - - + + + comic cómic - - - + + + web comic cómic web - - - + + + western manga (left to right) manga occidental (izquierda a derecha) - - + + Unable to delete No se ha podido borrar - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (de arriba a abajo) - + library? ? - + Are you sure? ¿Estás seguro? - + Rescan library for XML info Volver a escanear la biblioteca en busca de información XML - + Add new folder Añadir carpeta - + Delete folder Borrar carpeta - + Update folder Actualizar carpeta - + Upgrade failed La actualización falló - + There were errors during library upgrade in: Hubo errores durante la actualización de la biblioteca en: - - + + Copying comics... Copiando cómics... - - + + Moving comics... Moviendo cómics... - + Folder name: Nombre de la carpeta: - + No folder selected No has selecionado ninguna carpeta - + Please, select a folder first Por favor, selecciona una carpeta primero - + Error in path Error en la ruta - + There was an error accessing the folder's path Hubo un error al acceder a la ruta de la carpeta - + The selected folder and all its contents will be deleted from your disk. Are you sure? ¿Estás seguro de que deseas eliminar la carpeta seleccionada y todo su contenido de tu disco? - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Se produjo un problema al intentar eliminar las carpetas seleccionadas. Por favor, verifica los permisos de escritura y asegúrate de que no haya aplicaciones usando estas carpetas o alguno de los archivos contenidos en ellas. - + Add new reading lists Añadir nuevas listas de lectura - - + + List name: Nombre de la lista: - + Delete list/label Eliminar lista/etiqueta - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? El elemento seleccionado se eliminará, tus cómics o carpetas NO se eliminarán de tu disco. ¿Estás seguro? - + Rename list name Renombrar lista - - - - + + + + Set type Establecer tipo - + + Search filters + Filtros de búsqueda + + + + Unread + No leído + + + + In progress + En curso + + + + Highly rated + Con valoración alta + + + + Recently added + Añadido recientemente + + + + Search syntax… + Sintaxis de búsqueda… + + + + A repair of this library is already running (%1). Wait for it to finish. + Ya se está ejecutando una reparación de esta biblioteca (%1). Espere a que finalice. + + + + The library is locked by a repair that did not finish. + La biblioteca está bloqueada por una reparación que no finalizó. + + + + The library is locked by a repair started by %1. + La biblioteca está bloqueada por una reparación iniciada por %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Si está seguro de que no se está ejecutando ninguna otra reparación, se puede eliminar el bloqueo. ¿Eliminar el bloqueo y continuar? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Error al recuperar la restauración + + + Set custom cover Establecer portada personalizada - + Delete custom cover Eliminar portada personalizada - + Save covers Guardar portadas - + You are adding too many libraries. Estás añadiendo demasiadas bibliotecas. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1141,70 +1220,236 @@ Probablemente solo necesites una biblioteca en la carpeta principal de tus cómi YACReaderLibrary no te detendrá de crear más bibliotecas, pero deberías mantener el número de bibliotecas bajo control. - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader no encontrado. YACReader debería estar instalado en la misma carpeta que YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader no encontrado. Podría haber un problema con tu instalación de YACReader. - + Error Fallo - + Error opening comic with third party reader. Error al abrir el cómic con una aplicación de terceros. - + + + YACReader library database (*.ydb) + Base de datos de biblioteca de YACReader (*.ydb) + + + + The library database backup was created at: +%1 + La copia de seguridad de la base de datos de la biblioteca se creó en: +%1 + + + + Unable to create the library database backup: +%1 + No se pudo crear la copia de seguridad de la base de datos de la biblioteca: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Cierra YACReaderLibraryServer y cualquier otra aplicación YACReader que esté usando esta biblioteca antes de restaurarla. ¿Quieres continuar? + + + + Restoring library database... + Restaurando la base de datos de la biblioteca... + + + + The current library database is invalid. Restore the selected backup anyway? + La base de datos actual de la biblioteca no es válida. ¿Quieres restaurar de todos modos la copia seleccionada? + + + + + The library maintenance lock may be stale. Remove it and retry? + El bloqueo de mantenimiento de la biblioteca puede estar obsoleto. ¿Quieres eliminarlo y volver a intentarlo? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Reinicia YACReaderLibrary antes de volver a intentar la recuperación. + + + + The library database was restored successfully. Update the library now? + La base de datos de la biblioteca se restauró correctamente. ¿Quieres actualizar la biblioteca ahora? + + + + Library database damaged + Base de datos de la biblioteca dañada + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + La base de datos de la biblioteca '%1' está dañada, por lo que las actualizaciones, el mantenimiento y las copias de seguridad habituales no están disponibles. YACReader puede intentar reparar la base de datos. Es posible que algunos datos dañados no se puedan recuperar. Las copias de seguridad existentes no se modificarán. + + + + Attempt repair + Intentar reparar + + + + Restore a backup... + Restaurar una copia de seguridad... + + + + Repairing library database... + Reparando la base de datos de la biblioteca... + + + + + + Library database repair + Reparación de la base de datos de la biblioteca + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Otra operación de mantenimiento está usando esta biblioteca. Vuelve a intentarlo cuando termine. + + + + The library database is already valid. + La base de datos de la biblioteca ya es válida. + + + + Library database repaired + Base de datos de la biblioteca reparada + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + La base de datos de la biblioteca se reparó reconstruyendo sus índices. El original dañado se conservó en: +%1 + + + + Library database rebuilt + Base de datos de la biblioteca reconstruida + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + La base de datos de la biblioteca se reconstruyó correctamente. El original dañado se conservó en: +%1 + +¿Quieres actualizar la biblioteca ahora? + + + + + +The damaged original was preserved at: +%1 + + +El original dañado se conservó en: +%1 + + + + Library database repair failed + Error al reparar la base de datos de la biblioteca + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + No se pudo reparar la base de datos de la biblioteca: +%1%2 + +Puedes restaurar una copia de seguridad desde el menú Biblioteca o volver a crear la biblioteca. + + + + Remove and delete metadata and backups + Eliminar y borrar metadatos y copias de seguridad + + + Library info Información de la biblioteca - + Assign comics numbers Asignar números a los cómics - + Assign numbers starting in: Asignar números comenzando en: - + Invalid image Imagen inválida - + The selected file is not a valid image. El archivo seleccionado no es una imagen válida. - + Error saving cover Error guardando portada - + There was an error saving the cover image. Hubo un error guardando la image de portada. - + Remove comics Eliminar cómics - + Comics will only be deleted from the current label/list. Are you sure? Los cómics sólo se eliminarán de la etiqueta/lista actual. ¿Estás seguro? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Reparados: %1 +Fallidos: %2 +Archivos ausentes: %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary no te detendrá de crear más bibliotecas, pero deberías mante + Back up library database + Crear copia de seguridad de la base de datos + + + + Create a backup of the current library database + Crear una copia de seguridad de la base de datos actual de la biblioteca + + + + Restore library database backup + Restaurar copia de seguridad de la base de datos + + + + Restore the current library database from a backup + Restaurar la base de datos actual de la biblioteca desde una copia de seguridad + + + + + Repair covers and comic info + Reparar portadas e información de cómics + + + + Retry comics with missing covers or incomplete information + Volver a procesar cómics con portadas ausentes o información incompleta + + + Rename library Renombrar biblioteca - + Rename current library Renombrar la biblioteca seleccionada - + Remove library Eliminar biblioteca - + Remove current library from your collection Eliminar biblioteca de la colección - + Rescan library for XML info Volver a escanear la biblioteca en busca de información XML - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Intenta encontrar información XML incrustada en los archivos de cómic. Solo necesitas hacer esto si la biblioteca fue creada con la versión 9.8.2 o versiones anteriores o si estás utilizando software de terceros para incrustar información XML en los archivos. - + + Open library folder... + Abrir carpeta de la biblioteca... + + + + Open the root folder of the current library + Abrir la carpeta raíz de la biblioteca actual + + + Show library info Mostrar información de la biblioteca - + Show information about the current library Mostrar información de la biblioteca actual - + Open current comic Abrir cómic actual - + Open current comic on YACReader Abrir el cómic actual en YACReader - + Save selected covers to... Guardar las portadas seleccionadas en... - + Save covers of the selected comics as JPG files Guardar las portadas de los cómics seleccionados como archivos JPG - - + + Set as read Marcar como leído - + Set comic as read Marcar cómic como leído - - + + Set as unread Marcar como no leído - + Set comic as unread Marcar cómic como no leído - - + + manga historieta manga - + Set issue as manga Marcar número como manga - - + + comic cómic - + Set issue as normal Marcar número como cómic - + western manga manga occidental - + Set issue as western manga Marcar número como manga occidental - - + + web comic cómic web - + Set issue as web comic Marcar número como cómic web - - + + yonkoma tira yonkoma - + Set issue as yonkoma Marcar número como yonkoma - + Show/Hide marks Mostrar/Ocultar marcas - + Show or hide read marks Mostrar u ocultar marcas - + Show/Hide recent indicator Mostrar/Ocultar el indicador reciente - + Show or hide recent indicator Mostrar o ocultar el indicador reciente - - + + Fullscreen mode on/off Modo a pantalla completa on/off - + Help, About YACReader Ayuda, A cerca de... YACReader - + Add new folder Añadir carpeta - + Add new folder to the current library Añadir carpeta a la biblioteca actual - + Delete folder Borrar carpeta - + Delete current folder from disk Borrar carpeta actual del disco - + Select root node Seleccionar el nodo raíz - + Expand all nodes Expandir todos los nodos - + Collapse all nodes Contraer todos los nodos - + Show options dialog Mostrar opciones - + Show comics server options dialog Mostrar el diálogo de opciones del servidor de cómics - - + + Change between comics views Cambiar entre vistas de cómics - + Open folder... Abrir carpeta... - + Set as uncompleted Marcar como incompleto - + Set as completed Marcar como completo - + Set custom cover Establecer portada personalizada - + Delete custom cover Eliminar portada personalizada - + western manga (left to right) manga occidental (izquierda a derecha) - + Open containing folder... Abrir carpeta contenedora... - + Reset comic rating Reseteal cómic rating - + Select all comics Seleccionar todos los cómics - + Edit Editar - + Assign current order to comics Asignar el orden actual a los cómics - + Update cover Actualizar portada - + Delete selected comics Borrar los cómics seleccionados - + Delete metadata from selected comics Borrar metadatos de los cómics seleccionados - + Download tags from Comic Vine Descargar etiquetas de Comic Vine - + Focus search line Selecionar el campo de búsqueda - + Focus comics view Selecionar la vista de cómics - + Edit shortcuts Editar atajos - + &Quit &Salir - + Update folder Actualizar carpeta - + Update current folder Actualizar carpeta actual - + Scan legacy XML metadata Escaneal metadatos XML - + Add new reading list Añadir lista de lectura - + Add a new reading list to the current library Añadir una nueva lista de lectura a la biblioteca actual - + Remove reading list Eliminar lista de lectura - + Remove current reading list from the library Eliminar la lista de lectura actual de la biblioteca - + Add new label Añadir etiqueta - + Add a new label to this library Añadir etiqueta a esta biblioteca - + Rename selected list Renombrar la lista seleccionada - + Rename any selected labels or lists Renombrar las etiquetas o listas seleccionadas - + Add to... Añadir a... - + Favorites Favoritos - + Add selected comics to favorites list Añadir cómics seleccionados a la lista de favoritos @@ -1691,78 +1977,78 @@ YACReaderLibrary no te detendrá de crear más bibliotecas, pero deberías mante Apariencia - + Options Opciones - + Language Idioma - + Application language Idioma de la aplicación - + System default Predeterminado del sistema - + Tray icon settings (experimental) Opciones de bandeja de sistema (experimental) - + Close to tray Cerrar a la bandeja - + Start into the system tray Comenzar en la bandeja de sistema - + Edit Comic Vine API key Editar la clave API de Comic Vine - + Comic Vine API key Clave API de Comic Vine - + ComicInfo.xml legacy support Soporte para ComicInfo.xml - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importar metadatos desde ComicInfo.xml al añadir nuevos cómics - + Consider 'recent' items added or updated since X days ago Considerar elementos 'recientes' añadidos o actualizados desde hace X días - + Third party reader Lector externo - + Write {comic_file_path} where the path should go in the command Escribe {comic_file_path} donde la ruta al cómic debería ir en el comando - + Clear Borrar @@ -1922,7 +2208,7 @@ Para detener una actualización automática, toca en el indicador de carga junto Opciones generales - + Restart is needed Es necesario reiniciar @@ -2292,6 +2578,279 @@ Para detener una actualización automática, toca en el indicador de carga junto Número de volúmenes encontrados : %1 + + SearchFieldRegistry + + + Text, quoted text + Texto, texto entre comillas + + + + Integer + Entero + + + + Boolean (true / false) + Booleano (true / false) + + + + Integer (number of days) + Entero (número de días) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Enumeración (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Título del cómic + + + + Series name + Nombre de la serie + + + + Issue number + Número del ejemplar + + + + Volume identifier + Identificador del volumen + + + + Reading format + Formato de lectura + + + + Comic rating + Valoración del cómic + + + + Textual tags + Etiquetas de texto + + + + Writer credit + Crédito de guion + + + + Penciller credit + Crédito de dibujo + + + + Inker credit + Crédito de entintado + + + + Colorist credit + Crédito de color + + + + Letterer credit + Crédito de rotulación + + + + Cover artist credit + Crédito de portada + + + + Editor credit + Crédito de edición + + + + Story arc name + Nombre del arco argumental + + + + Position within a story arc + Posición dentro de un arco argumental + + + + Number of issues in a story arc + Número de ejemplares del arco argumental + + + + Characters appearing in the comic + Personajes que aparecen en el cómic + + + + Teams appearing in the comic + Equipos que aparecen en el cómic + + + + Locations appearing in the comic + Lugares que aparecen en el cómic + + + + Primary character or team + Personaje o equipo principal + + + + Comic synopsis + Sinopsis del cómic + + + + Publisher name + Nombre de la editorial + + + + Publishing imprint + Sello editorial + + + + Publication format + Formato de publicación + + + + Recommended age rating + Clasificación de edad recomendada + + + + Comic genre + Género del cómic + + + + ISO language code + Código de idioma ISO + + + + Publication date metadata + Metadatos de la fecha de publicación + + + + Series grouping metadata + Metadatos de agrupación de series + + + + Alternate series name + Nombre alternativo de la serie + + + + Alternate issue number + Número alternativo del ejemplar + + + + Alternate series issue count + Cantidad de ejemplares de la serie alternativa + + + + Number of issues in the series + Número de ejemplares de la serie + + + + Whether the comic is marked as read + Si el cómic está marcado como leído + + + + Whether reading has started + Si la lectura ha comenzado + + + + Whether metadata has been edited + Si se han editado los metadatos + + + + Whether the comic is in color + Si el cómic es en color + + + + Number of pages + Número de páginas + + + + Comic file name + Nombre del archivo del cómic + + + + When the item was added + Cuándo se añadió el elemento + + + + When the comic was last opened + Cuándo se abrió el cómic por última vez + + + + Comic notes + Notas del cómic + + + + Review text + Texto de la reseña + + + + Parent folder name + Nombre de la carpeta superior + + + + Default reading format for the folder + Formato de lectura predeterminado de la carpeta + + + + Whether the folder is complete + Si la carpeta está completa + + + + Whether the folder is marked as finished + Si la carpeta está marcada como finalizada + + + + When the folder was updated + Cuándo se actualizó la carpeta + + SearchSingleComic @@ -2311,6 +2870,301 @@ Para detener una actualización automática, toca en el indicador de carga junto Usar búsqueda exacta. Desactívala si quieres encontrar volúmenes que coincidan con algunas palabras del nombre. + + SearchSyntaxDialog + + + Common + Común + + + + Credits + Créditos + + + + Story + Historia + + + + Publication + Publicación + + + + Reading & files + Lectura y archivos + + + + Folders + Carpetas + + + + Search syntax + Sintaxis de búsqueda + + + + Search every comic and folder field, or build precise queries. + Busca en todos los campos de cómics y carpetas o crea consultas precisas. + + + + Quick guide + Guía rápida + + + + Fields (%1) + Campos (%1) + + + + Examples + Ejemplos + + + + Start with a simple search + Empieza con una búsqueda sencilla + + + + Just start typing. Plain text search across all metadata. + Solo tienes que empezar a escribir. El texto simple busca en todos los metadatos. + + + + 1. Search everywhere + 1. Buscar en todas partes + + + + Type any text or quoted text. + Escribe cualquier texto o texto entre comillas. + + + + 2. Target a field + 2. Buscar en un campo + + + + Use a field name followed by : or = + Usa el nombre de un campo seguido de : o = + + + + 3. Combine conditions + 3. Combinar condiciones + + + + Use AND, OR, NOT and parentheses. + Usa AND, OR, NOT y paréntesis. + + + + Operators + Operadores + + + + : or = + : o = + + + + contains the text + contiene el texto + + + + matches the complete value + coincide con el valor completo + + + + greater than / at least + mayor que / como mínimo + + + + less than / at most + menor que / como máximo + + + + "quoted text" + "texto entre comillas" + + + + keeps spaces inside one value + mantiene los espacios dentro de un único valor + + + + Dates and grouping + Fechas y agrupación + + + + added in the last 7 days + añadido en los últimos 7 días + + + + added more than 30 days ago + añadido hace más de 30 días + + + + group alternatives + agrupa alternativas + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Consejos: los espacios funcionan como AND y las búsquedas no distinguen entre mayúsculas y minúsculas. Usa comillas para incluir espacios en un valor. + + + + Find a field… + Buscar un campo… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + El texto puede introducirse directamente o entre comillas. Los campos de enteros admiten <, <=, > y >=. En los campos de fecha, el entero es un número de días (added>7 significa añadido en los últimos 7 días). + + + + Field + Campo + + + + Description + Descripción + + + + Input + Entrada + + + + Example + Ejemplo + + + + Examples show the pattern—replace the values with your own. + Los ejemplos muestran el patrón; sustituye los valores por los tuyos. + + + + Query + Consulta + + + + What it finds + Qué encuentra + + + + Common filters + Filtros comunes + + + + Unread comics + Cómics no leídos + + + + Comics in progress + Cómics en curso + + + + Highly rated comics + Cómics con valoración alta + + + + Comics added in the last 7 days + Cómics añadidos en los últimos 7 días + + + + Metadata + Metadatos + + + + Search by series + Buscar por serie + + + + Search by writer + Buscar por guionista + + + + Manga comics + Cómics manga + + + + Search textual tags + Buscar etiquetas de texto + + + + Advanced combinations + Combinaciones avanzadas + + + + Match either writer + Coincide con cualquiera de los guionistas + + + + Group alternatives + Agrupar alternativas + + + + Exclude a value + Excluir un valor + + + + Older, highly rated comics + Cómics antiguos con valoración alta + + + + Copy query + Copiar consulta + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Los espacios funcionan como AND. Usa comillas para las frases y paréntesis para controlar la agrupación. + + SearchVolume @@ -2416,39 +3270,81 @@ Para detener una actualización automática, toca en el indicador de carga junto ServerConfigDialog - + + + Server connectivity + Conectividad del servidor + + + + Scan to connect + Escanea para conectar + + + + Devices on this network can reach your library at the address below. + Los dispositivos de esta red pueden acceder a tu biblioteca en la dirección que aparece a continuación. + + + + IP address + Dirección IP + + + Port Puerto - + + Web interface + Interfaz web + + + + Copy link + Copiar enlace + + + + Open web UI + Abrir interfaz web + + + + Enable the server + Activar el servidor + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader está disponible para iOS y Android. Descúbrelo para <a href='https://ios.yacreader.com'>iOS</a> o <a href='https://android.yacreader.com'>Android</a>. + + enable the server - activar el servidor + activar el servidor - - set port - fijar puerto + + Set port + set port + Establecer puerto - Server connectivity information - Infomación de conexión del servidor + Infomación de conexión del servidor - Scan it! - ¡Escaneálo! + ¡Escaneálo! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader está disponible para iOS y Android.<br/> Descúbrela para <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a>o <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader está disponible para iOS y Android.<br/> Descúbrela para <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a>o <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Elige una dirección IP + Elige una dirección IP @@ -2867,14 +3763,14 @@ Para detener una actualización automática, toca en el indicador de carga junto YACReader::WhatsNewDialog - + Release notes are not available. - + Las notas de la versión no están disponibles. - + Previous versions - + Versiones anteriores @@ -2910,22 +3806,32 @@ Para detener una actualización automática, toca en el indicador de carga junto YACReaderOptionsDialog - + Save Guardar - + Cancel Cancelar - + + Keyboard shortcuts + Atajos de teclado + + + + Customize the keyboard shortcuts used by the application. + Personaliza los atajos de teclado utilizados por la aplicación. + + + Edit shortcuts Editar atajos - + Shortcuts Atajos @@ -2933,7 +3839,12 @@ Para detener una actualización automática, toca en el indicador de carga junto YACReaderSearchLineEdit - + + Search filters + Filtros de búsqueda + + + type to search escribe para buscar diff --git a/YACReaderLibrary/yacreaderlibrary_fr.ts b/YACReaderLibrary/yacreaderlibrary_fr.ts index 339a24839..52135d086 100644 --- a/YACReaderLibrary/yacreaderlibrary_fr.ts +++ b/YACReaderLibrary/yacreaderlibrary_fr.ts @@ -166,24 +166,24 @@ Le thème actuel JSON n'a pas pu être chargé. - + Import theme Importer un thème - + JSON files (*.json);;All files (*) Fichiers JSON (*.json);;Tous les fichiers (*) - + Could not import theme from: %1 Impossible d'importer le thème depuis : %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Échec de l'importation @@ -293,67 +293,67 @@ ComicModel - + no non - + yes oui - + Read Lu - + Series Série - + Volume Tome - + Story Arc Arc d'histoire - + Size Taille - + Pages Feuilles - + Title Titre - + Current Page Page en cours - + File Name Nom du fichier - + Publication Date Date de publication - + Rating Note @@ -617,25 +617,30 @@ FileComic - + 7z not found 7z introuvable - + CRC error on page (%1): some of the pages will not be displayed correctly Erreur CRC sur la page (%1): certaines pages ne s'afficheront pas correctement - + Unknown error opening the file Erreur inconnue lors de l'ouverture du fichier - + Format not supported Format non supporté + + + Unsupported EPUB: %1 + + FolderContentView @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Aide - + System info Informations système - + + Changelog + Journal des modifications + + + About A propos @@ -789,142 +799,146 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>La bibliothèque actuelle est en cours d'analyse pour rechercher des informations sur les métadonnées XML héritées.</p><p>Ceci n'est nécessaire qu'une seule fois, et uniquement si la bibliothèque a été créée avec YACReaderLibrary 9.8.2 ou une version antérieure.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>La bibliothèque actuelle est analysée pour rechercher les couvertures manquantes et les informations de BD incomplètes.</p><p>Cette opération peut prendre plusieurs minutes. Vous pouvez l'arrêter et la relancer plus tard.</p> + LibraryWindow - + The selected folder doesn't contain any library. Le dossier sélectionné ne contient aucune librairie. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Cette librairie a été créée avec une ancienne version de YACReaderLibrary. Mise à jour necessaire. Mettre à jour? - + Comic Bande dessinée - + Error opening the library Erreur lors de l'ouverture de la librairie - - - + + + manga mangas - - - + + + comic comique - - - + + + western manga (left to right) manga occidental (de gauche à droite) - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (de haut en bas) - Remove and delete metadata - Supprimer les métadata + Supprimer les métadata - + Old library Ancienne librairie - + Set as completed Marquer comme complet - + Library Librairie - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Cette librairie a été créée avec une version plus récente de YACReaderLibrary. Télécharger la nouvelle version? - - + + Moving comics... Déplacer la bande dessinée... - - + + Copying comics... Copier la bande dessinée... - + Library '%1' is no longer available. Do you want to remove it? La librarie '%1' n'est plus disponible. Voulez-vous la supprimer? - + Open folder... Ouvrir le dossier... - + Do you want remove Voulez-vous supprimer - + Set as uncompleted Marquer comme incomplet - + Error updating the library Erreur lors de la mise à jour de la librairie - + Folder Dossier - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? L'élément sélectionné sera supprimé, vos bandes dessinées ou dossiers ne seront pas supprimés de votre disque. Êtes-vous sûr? - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? La librarie '%1' a été créée avec une ancienne version de YACReaderLibrary. Elle doit être re-créée. Voulez-vous créer la librairie? - + Add new reading lists Ajouter de nouvelles listes de lecture - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -937,274 +951,505 @@ Vous n'avez probablement besoin que d'une bibliothèque dans votre dos YACReaderLibrary ne vous empêchera pas de créer plus de bibliothèques, mais vous devriez garder le nombre de bibliothèques bas. - + Set as read Marquer comme lu - + Library not available Librairie non disponible - + YACReader Library Librairie de YACReader - + Error creating the library Erreur lors de la création de la librairie - + Update folder Mettre à jour le dossier - + Update needed Mise à jour requise - + Library name already exists Le nom de la librairie existe déjà - + There is another library with the name '%1'. Une autre librairie a le nom '%1'. - + Download new version Téléchrger la nouvelle version - + Delete comics Supprimer les comics - + All the selected comics will be deleted from your disk. Are you sure? Tous les comics sélectionnés vont être supprimés de votre disque. Êtes-vous sûr? - - + + Set as unread Marquer comme non-lu - + Library not found Librairie introuvable - + library? la librairie? - + Are you sure? Êtes-vous sûr? - + Rescan library for XML info Réanalyser la bibliothèque pour les informations XML - - - + + + web comic bande dessinée Web - + Add new folder Ajouter un nouveau dossier - + Delete folder Supprimer le dossier - + Upgrade failed La mise à niveau a échoué - + There were errors during library upgrade in: Des erreurs se sont produites lors de la mise à niveau de la bibliothèque dans : - + Folder name: Nom du dossier : - + No folder selected Aucun dossier sélectionné - + Please, select a folder first Veuillez d'abord sélectionner un dossier - + Error in path Erreur dans le chemin - + There was an error accessing the folder's path Une erreur s'est produite lors de l'accès au chemin du dossier - + The selected folder and all its contents will be deleted from your disk. Are you sure? Le dossier sélectionné et tout son contenu seront supprimés de votre disque. Es-tu sûr? - - + + Unable to delete Impossible de supprimer - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Un problème est survenu lors de la tentative de suppression des dossiers sélectionnés. Veuillez vérifier les autorisations d'écriture et assurez-vous que toutes les applications utilisent ces dossiers ou l'un des fichiers contenus. - - + + List name: Nom de la liste : - + Delete list/label Supprimer la liste/l'étiquette - + Rename list name Renommer le nom de la liste - - - - + + + + Set type Définir le type - + + Search filters + Filtres de recherche + + + + Unread + Non lus + + + + In progress + En cours + + + + Highly rated + Très bien notés + + + + Recently added + Ajoutés récemment + + + + Search syntax… + Syntaxe de recherche… + + + + A repair of this library is already running (%1). Wait for it to finish. + Une réparation de cette librairie est déjà en cours (%1). Attendez qu'elle se termine. + + + + The library is locked by a repair that did not finish. + La librairie est verrouillée par une réparation qui ne s'est pas terminée. + + + + The library is locked by a repair started by %1. + La librairie est verrouillée par une réparation démarrée par %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Si vous êtes sûr qu'aucune autre réparation n'est en cours, le verrou peut être supprimé. Supprimer le verrou et continuer ? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Échec de la récupération de la restauration + + + Set custom cover Définir une couverture personnalisée - + Delete custom cover Supprimer la couverture personnalisée - + Save covers Enregistrer les couvertures - + You are adding too many libraries. Vous ajoutez trop de bibliothèques. - - + + YACReader not found YACReader introuvable - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader introuvable. YACReader doit être installé dans le même dossier que YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader introuvable. Il se peut qu'il y ait un problème avec votre installation de YACReader. - + Error Erreur - + Error opening comic with third party reader. Erreur lors de l'ouverture de la bande dessinée avec un lecteur tiers. - + + + YACReader library database (*.ydb) + Base de données de bibliothèque YACReader (*.ydb) + + + + The library database backup was created at: +%1 + La sauvegarde de la base de données de la bibliothèque a été créée ici : +%1 + + + + Unable to create the library database backup: +%1 + Impossible de créer la sauvegarde de la base de données de la bibliothèque : +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Fermez YACReaderLibraryServer et toute autre application YACReader utilisant cette bibliothèque avant la restauration. Continuer ? + + + + Restoring library database... + Restauration de la base de données de la bibliothèque... + + + + The current library database is invalid. Restore the selected backup anyway? + La base de données actuelle de la bibliothèque n'est pas valide. Restaurer quand même la sauvegarde sélectionnée ? + + + + + The library maintenance lock may be stale. Remove it and retry? + Le verrou de maintenance de la bibliothèque est peut-être obsolète. Le supprimer et réessayer ? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Redémarrez YACReaderLibrary avant de tenter à nouveau la récupération. + + + + The library database was restored successfully. Update the library now? + La base de données de la bibliothèque a été restaurée. Mettre à jour la bibliothèque maintenant ? + + + + Library database damaged + Base de données de la bibliothèque endommagée + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + La base de données de la bibliothèque « %1 » est endommagée. Les mises à jour, la maintenance et les sauvegardes habituelles sont donc indisponibles. YACReader peut tenter de réparer la base de données. Certaines données endommagées peuvent être irrécupérables. Les sauvegardes existantes ne seront pas modifiées. + + + + Attempt repair + Tenter la réparation + + + + Restore a backup... + Restaurer une sauvegarde... + + + + Repairing library database... + Réparation de la base de données... + + + + + + Library database repair + Réparation de la base de données de la bibliothèque + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Une autre opération de maintenance utilise actuellement cette bibliothèque. Réessayez lorsqu'elle sera terminée. + + + + The library database is already valid. + La base de données de la bibliothèque est déjà valide. + + + + Library database repaired + Base de données de la bibliothèque réparée + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + La base de données de la bibliothèque a été réparée en reconstruisant ses index. L'original endommagé a été conservé ici : +%1 + + + + Library database rebuilt + Base de données de la bibliothèque reconstruite + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + La base de données de la bibliothèque a été reconstruite. L'original endommagé a été conservé ici : +%1 + +Mettre à jour la bibliothèque maintenant ? + + + + + +The damaged original was preserved at: +%1 + + +L'original endommagé a été conservé ici : +%1 + + + + Library database repair failed + Échec de la réparation de la base de données + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + La base de données de la bibliothèque n'a pas pu être réparée : +%1%2 + +Vous pouvez restaurer une sauvegarde depuis le menu Bibliothèque ou recréer la bibliothèque. + + + + Remove and delete metadata and backups + Retirer et supprimer les métadonnées et les sauvegardes + + + Library info Informations sur la bibliothèque - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Un problème est survenu lors de la tentative de suppression des bandes dessinées sélectionnées. Veuillez vérifier les autorisations d'écriture dans les fichiers sélectionnés ou le dossier contenant. - + Assign comics numbers Attribuer des numéros de bandes dessinées - + Assign numbers starting in: Attribuez des numéros commençant par : - + Invalid image Image invalide - + The selected file is not a valid image. Le fichier sélectionné n'est pas une image valide. - + Error saving cover Erreur lors de l'enregistrement de la couverture - + There was an error saving the cover image. Une erreur s'est produite lors de l'enregistrement de l'image de couverture. - + Remove comics Supprimer les bandes dessinées - + Comics will only be deleted from the current label/list. Are you sure? Les bandes dessinées seront uniquement supprimées du label/liste actuelle. Es-tu sûr? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Réparés : %1 +Échecs : %2 +Fichiers manquants : %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary ne vous empêchera pas de créer plus de bibliothèques, mais v + Back up library database + Sauvegarder la base de données de la bibliothèque + + + + Create a backup of the current library database + Créer une sauvegarde de la base de données actuelle de la bibliothèque + + + + Restore library database backup + Restaurer une sauvegarde de la base de données + + + + Restore the current library database from a backup + Restaurer la base de données actuelle de la bibliothèque depuis une sauvegarde + + + + + Repair covers and comic info + Réparer les couvertures et les informations des BD + + + + Retry comics with missing covers or incomplete information + Réessayer les BD dont la couverture est manquante ou les informations incomplètes + + + Rename library Renommer la librairie - + Rename current library Renommer la librairie actuelle - + Remove library Supprimer la librairie - + Remove current library from your collection Enlever cette librairie de votre collection - + Rescan library for XML info Réanalyser la bibliothèque pour les informations XML - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Essaie de trouver des informations XML intégrées dans des fichiers de bandes dessinées. Vous ne devez le faire que si la bibliothèque a été créée avec la version 9.8.2 ou des versions antérieures ou si vous utilisez un logiciel tiers pour intégrer des informations XML dans les fichiers. - + + Open library folder... + Ouvrir le dossier de la bibliothèque... + + + + Open the root folder of the current library + Ouvrir le dossier racine de la bibliothèque actuelle + + + Show library info Afficher les informations sur la bibliothèque - + Show information about the current library Afficher des informations sur la bibliothèque actuelle - + Open current comic Ouvrir cette bande dessinée - + Open current comic on YACReader Ouvrir cette bande dessinée dans YACReader - + Save selected covers to... Exporter la couverture vers... - + Save covers of the selected comics as JPG files Enregistrer les couvertures des bandes dessinées sélectionnées en tant que fichiers JPG - - + + Set as read Marquer comme lu - + Set comic as read Marquer cette bande dessinée comme lu - - + + Set as unread Marquer comme non-lu - + Set comic as unread Marquer cette bande dessinée comme non-lu - - + + manga mangas - + Set issue as manga Définir le problème comme manga - - + + comic comique - + Set issue as normal Définir le problème comme d'habitude - + western manga manga occidental - + Set issue as western manga Définir le problème comme un manga occidental - - + + web comic bande dessinée Web - + Set issue as web comic Définir le problème comme bande dessinée Web - - + + yonkoma Yonkoma - + Set issue as yonkoma Définir le problème comme Yonkoma - + Show/Hide marks Afficher/Cacher les marqueurs - + Show or hide read marks Afficher ou masquer les marques de lecture - + Show/Hide recent indicator Afficher/Masquer l'indicateur récent - + Show or hide recent indicator Afficher ou masquer l'indicateur récent - - + + Fullscreen mode on/off Mode plein écran activé/désactivé - + Help, About YACReader Aide, à propos de YACReader - + Add new folder Ajouter un nouveau dossier - + Add new folder to the current library Ajouter un nouveau dossier à la bibliothèque actuelle - + Delete folder Supprimer le dossier - + Delete current folder from disk Supprimer le dossier actuel du disque - + Select root node Allerà la racine - + Expand all nodes Afficher tous les noeuds - + Collapse all nodes Réduire tous les nœuds - + Show options dialog Ouvrir la boite de dialogue - + Show comics server options dialog Ouvrir la boite de dialogue du serveur - - + + Change between comics views Changement entre les vues de bandes dessinées - + Open folder... Ouvrir le dossier... - + Set as uncompleted Marquer comme incomplet - + Set as completed Marquer comme complet - + Set custom cover Définir une couverture personnalisée - + Delete custom cover Supprimer la couverture personnalisée - + western manga (left to right) manga occidental (de gauche à droite) - + Open containing folder... Ouvrir le dossier... - + Reset comic rating Supprimer la note d'évaluation - + Select all comics Sélectionner toutes les bandes dessinées - + Edit Editer - + Assign current order to comics Assigner l'ordre actuel aux bandes dessinées - + Update cover Mise à jour des couvertures - + Delete selected comics Supprimer la bande dessinée sélectionnée - + Delete metadata from selected comics Supprimer les métadonnées des bandes dessinées sélectionnées - + Download tags from Comic Vine Télécharger les informations de Comic Vine - + Focus search line Ligne de recherche ciblée - + Focus comics view Focus sur la vue des bandes dessinées - + Edit shortcuts Modifier les raccourcis - + &Quit &Quitter - + Update folder Mettre à jour le dossier - + Update current folder Mettre à jour ce dossier - + Scan legacy XML metadata Analyser les métadonnées XML héritées - + Add new reading list Ajouter une nouvelle liste de lecture - + Add a new reading list to the current library Ajouter une nouvelle liste de lecture à la bibliothèque actuelle - + Remove reading list Supprimer la liste de lecture - + Remove current reading list from the library Supprimer la liste de lecture actuelle de la bibliothèque - + Add new label Ajouter une nouvelle étiquette - + Add a new label to this library Ajouter une nouvelle étiquette à cette bibliothèque - + Rename selected list Renommer la liste sélectionnée - + Rename any selected labels or lists Renommer toutes les étiquettes ou listes sélectionnées - + Add to... Ajouter à... - + Favorites Favoris - + Add selected comics to favorites list Ajouter la bande dessinée sélectionnée à la liste des favoris @@ -1691,78 +1977,78 @@ YACReaderLibrary ne vous empêchera pas de créer plus de bibliothèques, mais v Apparence - + Options Possibilités - + Language Langue - + Application language Langue de l'application - + System default Par défaut du système - + Tray icon settings (experimental) Paramètres de l'icône de la barre d'état (expérimental) - + Close to tray Près du plateau - + Start into the system tray Commencez dans la barre d'état système - + Edit Comic Vine API key Modifier la clé API Comic Vine - + Comic Vine API key Clé API Comic Vine - + ComicInfo.xml legacy support Prise en charge héritée de ComicInfo.xml - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importer des métadonnées depuis ComicInfo.xml lors de l'ajout de nouvelles bandes dessinées - + Consider 'recent' items added or updated since X days ago Considérez les éléments « récents » ajoutés ou mis à jour depuis X jours - + Third party reader Lecteur tiers - + Write {comic_file_path} where the path should go in the command Écrivez {comic_file_path} où le chemin doit aller dans la commande - + Clear Clair @@ -1922,7 +2208,7 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha Général - + Restart is needed Redémarrage nécessaire @@ -2292,6 +2578,279 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha Nombre de %1 trouvés : %2 + + SearchFieldRegistry + + + Text, quoted text + Texte, texte entre guillemets + + + + Integer + Entier + + + + Boolean (true / false) + Booléen (true / false) + + + + Integer (number of days) + Entier (nombre de jours) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Énumération (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Titre de la bande dessinée + + + + Series name + Nom de la série + + + + Issue number + Numéro du fascicule + + + + Volume identifier + Identifiant du volume + + + + Reading format + Format de lecture + + + + Comic rating + Note de la bande dessinée + + + + Textual tags + Étiquettes textuelles + + + + Writer credit + Crédit du scénariste + + + + Penciller credit + Crédit du dessinateur + + + + Inker credit + Crédit de l'encreur + + + + Colorist credit + Crédit du coloriste + + + + Letterer credit + Crédit du lettreur + + + + Cover artist credit + Crédit de l'artiste de couverture + + + + Editor credit + Crédit de l'éditeur + + + + Story arc name + Nom de l'arc narratif + + + + Position within a story arc + Position dans un arc narratif + + + + Number of issues in a story arc + Nombre de fascicules dans un arc narratif + + + + Characters appearing in the comic + Personnages apparaissant dans la bande dessinée + + + + Teams appearing in the comic + Équipes apparaissant dans la bande dessinée + + + + Locations appearing in the comic + Lieux apparaissant dans la bande dessinée + + + + Primary character or team + Personnage ou équipe principale + + + + Comic synopsis + Synopsis de la bande dessinée + + + + Publisher name + Nom de la maison d'édition + + + + Publishing imprint + Label éditorial + + + + Publication format + Format de publication + + + + Recommended age rating + Classification d'âge recommandée + + + + Comic genre + Genre de la bande dessinée + + + + ISO language code + Code de langue ISO + + + + Publication date metadata + Métadonnées de date de publication + + + + Series grouping metadata + Métadonnées de regroupement des séries + + + + Alternate series name + Autre nom de la série + + + + Alternate issue number + Autre numéro de fascicule + + + + Alternate series issue count + Nombre de fascicules de l'autre série + + + + Number of issues in the series + Nombre de fascicules dans la série + + + + Whether the comic is marked as read + Si la bande dessinée est marquée comme lue + + + + Whether reading has started + Si la lecture a commencé + + + + Whether metadata has been edited + Si les métadonnées ont été modifiées + + + + Whether the comic is in color + Si la bande dessinée est en couleur + + + + Number of pages + Nombre de pages + + + + Comic file name + Nom du fichier de la bande dessinée + + + + When the item was added + Date d'ajout de l'élément + + + + When the comic was last opened + Dernière ouverture de la bande dessinée + + + + Comic notes + Notes de la bande dessinée + + + + Review text + Texte de la critique + + + + Parent folder name + Nom du dossier parent + + + + Default reading format for the folder + Format de lecture par défaut du dossier + + + + Whether the folder is complete + Si le dossier est complet + + + + Whether the folder is marked as finished + Si le dossier est marqué comme terminé + + + + When the folder was updated + Date de mise à jour du dossier + + SearchSingleComic @@ -2311,6 +2870,301 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha Utilisez la recherche de correspondance exacte. Désactivez-la si vous souhaitez rechercher des volumes correspondant à certains mots du nom. + + SearchSyntaxDialog + + + Common + Commun + + + + Credits + Crédits + + + + Story + Histoire + + + + Publication + Publication + + + + Reading & files + Lecture et fichiers + + + + Folders + Dossiers + + + + Search syntax + Syntaxe de recherche + + + + Search every comic and folder field, or build precise queries. + Recherchez dans tous les champs des bandes dessinées et des dossiers, ou créez des requêtes précises. + + + + Quick guide + Guide rapide + + + + Fields (%1) + Champs (%1) + + + + Examples + Exemples + + + + Start with a simple search + Commencer par une recherche simple + + + + Just start typing. Plain text search across all metadata. + Commencez simplement à saisir du texte. Le texte simple est recherché dans toutes les métadonnées. + + + + 1. Search everywhere + 1. Rechercher partout + + + + Type any text or quoted text. + Saisissez du texte ou du texte entre guillemets. + + + + 2. Target a field + 2. Cibler un champ + + + + Use a field name followed by : or = + Utilisez un nom de champ suivi de : ou = + + + + 3. Combine conditions + 3. Combiner des conditions + + + + Use AND, OR, NOT and parentheses. + Utilisez AND, OR, NOT et des parenthèses. + + + + Operators + Opérateurs + + + + : or = + : ou = + + + + contains the text + contient le texte + + + + matches the complete value + correspond à la valeur complète + + + + greater than / at least + supérieur à / au moins + + + + less than / at most + inférieur à / au plus + + + + "quoted text" + "texte entre guillemets" + + + + keeps spaces inside one value + conserve les espaces dans une seule valeur + + + + Dates and grouping + Dates et regroupement + + + + added in the last 7 days + ajouté au cours des 7 derniers jours + + + + added more than 30 days ago + ajouté il y a plus de 30 jours + + + + group alternatives + regroupe les alternatives + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Conseils : les espaces agissent comme AND et les recherches ne tiennent pas compte de la casse. Utilisez des guillemets pour inclure des espaces dans une valeur. + + + + Find a field… + Rechercher un champ… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Le texte peut être saisi directement ou entre guillemets. Les champs entiers acceptent <, <=, > et >=. Pour les champs de date, l'entier est un nombre de jours (added>7 signifie ajouté au cours des 7 derniers jours). + + + + Field + Champ + + + + Description + Description + + + + Input + Saisie + + + + Example + Exemple + + + + Examples show the pattern—replace the values with your own. + Les exemples montrent le modèle — remplacez les valeurs par les vôtres. + + + + Query + Requête + + + + What it finds + Résultat + + + + Common filters + Filtres courants + + + + Unread comics + Bandes dessinées non lues + + + + Comics in progress + Bandes dessinées en cours de lecture + + + + Highly rated comics + Bandes dessinées très bien notées + + + + Comics added in the last 7 days + Bandes dessinées ajoutées au cours des 7 derniers jours + + + + Metadata + Métadonnées + + + + Search by series + Rechercher par série + + + + Search by writer + Rechercher par scénariste + + + + Manga comics + Mangas + + + + Search textual tags + Rechercher les étiquettes textuelles + + + + Advanced combinations + Combinaisons avancées + + + + Match either writer + Correspond à l'un des scénaristes + + + + Group alternatives + Regrouper les alternatives + + + + Exclude a value + Exclure une valeur + + + + Older, highly rated comics + Bandes dessinées anciennes et très bien notées + + + + Copy query + Copier la requête + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Les espaces agissent comme AND. Utilisez des guillemets pour les expressions et des parenthèses pour contrôler le regroupement. + + SearchVolume @@ -2416,39 +3270,81 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha ServerConfigDialog - + + + Server connectivity + Connectivité du serveur + + + + Scan to connect + Scanner pour se connecter + + + + Devices on this network can reach your library at the address below. + Les appareils de ce réseau peuvent accéder à votre bibliothèque à l’adresse ci-dessous. + + + + IP address + Adresse IP + + + Port Port r?seau - + + Web interface + Interface web + + + + Copy link + Copier le lien + + + + Open web UI + Ouvrir l’interface web + + + + Enable the server + Activer le serveur + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader est disponible pour iOS et Android. Découvrez-le pour <a href='https://ios.yacreader.com'>iOS</a> ou <a href='https://android.yacreader.com'>Android</a>. + + enable the server - Autoriser le serveur + Autoriser le serveur - - set port - Configurer le port + + Set port + set port + Définir le port - Server connectivity information - Informations sur la connectivité du serveur + Informations sur la connectivité du serveur - Scan it! - Scannez-le ! + Scannez-le ! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader est disponible pour les appareils iOS et Android.<br/>Découvrez-le pour <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> ou <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader est disponible pour les appareils iOS et Android.<br/>Découvrez-le pour <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> ou <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Choisissez une adresse IP + Choisissez une adresse IP @@ -2867,14 +3763,14 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha YACReader::WhatsNewDialog - + Release notes are not available. - + Les notes de version ne sont pas disponibles. - + Previous versions - + Versions précédentes @@ -2910,22 +3806,32 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha YACReaderOptionsDialog - + Save Sauvegarder - + Cancel Annuler - + + Keyboard shortcuts + Raccourcis clavier + + + + Customize the keyboard shortcuts used by the application. + Personnalisez les raccourcis clavier utilisés par l’application. + + + Edit shortcuts Modifier les raccourcis - + Shortcuts Raccourcis @@ -2933,7 +3839,12 @@ Pour arrêter une mise à jour automatique, appuyez sur l'indicateur de cha YACReaderSearchLineEdit - + + Search filters + Filtres de recherche + + + type to search tapez pour rechercher diff --git a/YACReaderLibrary/yacreaderlibrary_it.ts b/YACReaderLibrary/yacreaderlibrary_it.ts index 1db4dad06..0f484bc54 100644 --- a/YACReaderLibrary/yacreaderlibrary_it.ts +++ b/YACReaderLibrary/yacreaderlibrary_it.ts @@ -166,24 +166,24 @@ Impossibile caricare il tema corrente JSON. - + Import theme Importa tema - + JSON files (*.json);;All files (*) File JSON (*.json);;Tutti i file (*) - + Could not import theme from: %1 Impossibile importare il tema da: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Importazione non riuscita @@ -293,67 +293,67 @@ ComicModel - + no No - + yes Si - + Read Leggi - + Series Serie - + Volume Tomo - + Story Arc Arco narrativo - + Size Dimensione - + Pages Pagine - + Title Titolo - + Current Page Pagina corrente - + File Name Nome file - + Publication Date Data di pubblicazione - + Rating Valutazione @@ -617,22 +617,27 @@ FileComic - + Format not supported Formato non supportato - + 7z not found 7z non trovato - + Unknown error opening the file Errore sconosciuto all'apertura del file - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Errore CRC alla pagina (%1): alcune pagine potrebbero non essere visualizzate correttamente @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Aiuto - + System info Informazioni di sistema - + + Changelog + Registro modifiche + + + About Informazioni @@ -789,165 +799,169 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>Scansione della libreria corrente per informazioni sui metadati XML legacy.</p><p>Questa operazione è necessaria solo una volta e solo se la libreria è stata creata con YACReaderLibrary 9.8.2 o versioni precedenti.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>La libreria corrente viene controllata per individuare copertine mancanti e informazioni incomplete sui fumetti.</p><p>L'operazione può richiedere diversi minuti. Puoi interromperla ed eseguirla di nuovo in seguito.</p> + LibraryWindow - + The selected folder doesn't contain any library. La cartella selezionata non contiene nessuna Libreria. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Questa libreria è stata creata con una versione precedente di YACREaderLibrary. Deve essere aggiornata. Aggiorno ora? - + Comic Fumetto - + Folder name: Nome della cartella: - + The selected folder and all its contents will be deleted from your disk. Are you sure? La cartella seleziona e tutto il suo contenuto verranno cancellati dal tuo disco. Sei sicuro? - + Error opening the library Errore nell'apertura della libreria - - + + YACReader not found YACReader non trovato - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. C'è stato un problema cancellando le cartelle selezionate. Per favore controlla i tuoi permessi di scrittura e sii sicuro che non ci siano altre applicazioni che usano le stesse cartelle. - + Rename list name Rinomina la lista - Remove and delete metadata - Rimuovi e cancella i Metadati + Rimuovi e cancella i Metadati - + Old library Vecchia libreria - + Set as completed Segna come completo - + There was an error accessing the folder's path C'è stato un errore nell'accesso al percorso della cartella - + Library Libreria - + Comics will only be deleted from the current label/list. Are you sure? I fumetti verranno cancellati dall'etichetta/lista corrente. Sei sicuro? - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Questa libreria è stata creata con una verisone più recente di YACReaderLibrary. Scarico la versione aggiornata ora? - - + + Moving comics... Sto muovendo i fumetti... - - + + Copying comics... Sto copiando i fumetti... - + Library '%1' is no longer available. Do you want to remove it? La libreria '%1' non è più disponibile, la vuoi cancellare? - + Open folder... Apri Cartella... - + Do you want remove Vuoi rimuovere - + Set as uncompleted Segna come non completo - + Error in path Errore nel percorso - + Error updating the library Errore aggiornando la libreria - + Folder Cartella - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? Gli elementi selezionati verranno cancellati, i tuoi fumetti o cartella NON verranno cancellati dal tuo disco. Sei sicuro? - - + + List name: Nome lista: - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? La libreria '%1' è stata creata con una versione precedente di YACREaderLibrary. Deve essere ricreata. Lo vuoi fare ora? - + Save covers Salva Copertine - + Add new reading lists Aggiungi una lista di lettura - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -960,250 +974,481 @@ Hai probabilemnte bisogno di una sola Libreria al livello superiore, puoi poi na YACReader non ti fermerà dal creare altre librerie ma è meglio se terrai il numero di librerie basso. - + Set as read Setta come letto - + Library info Informazioni sulla biblioteca - + Assign comics numbers Assegna un numero ai fumetti - + Please, select a folder first Per cortesia prima seleziona una cartella - + Library not available Libreria non disponibile - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. C'è un problema nel cancellare i fumetti selezionati. Per favore controlla i tuoi permessi di scrittura sui file o sulla cartella. - + YACReader Library Libreria YACReader - + Error creating the library Errore creando la libreria - + You are adding too many libraries. Stai aggiungendto troppe librerie. - + Update folder Aggiorna Cartella - + Update needed Devi aggiornarmi - + Library name already exists Esiste già una libreria con lo stesso nome - + There is another library with the name '%1'. Esiste già una libreria con il nome '%1'. - + Delete folder Cancella Cartella - + Assign numbers starting in: Assegna numeri partendo da: - + Download new version Scarica la nuova versione - + + Remove and delete metadata and backups + Rimuovi ed elimina metadati e backup + + + Invalid image Immagine non valida - + The selected file is not a valid image. Il file selezionato non è un'immagine valida. - + Error saving cover Errore durante il salvataggio della copertina - + There was an error saving the cover image. Si è verificato un errore durante il salvataggio dell'immagine di copertina. - + Delete comics Cancella i fumetti - + Add new folder Aggiungi una nuova cartella - + Delete list/label Cancella Lista/Etichetta - + No folder selected Nessuna cartella selezionata - + All the selected comics will be deleted from your disk. Are you sure? Tutti i fumetti selezionati saranno cancellati dal tuo disco. Sei sicuro? - + Remove comics Rimuovi i fumetti - - + + Set as unread Setta come non letto - + Library not found Libreria non trovata - - - + + + manga Manga - - - + + + comic comico - - - + + + web comic fumetto web - - - + + + western manga (left to right) manga occidentale (da sinistra a destra) - - + + Unable to delete Non posso cancellare - - - + + + 4koma (top to botom) 4koma (dall'alto verso il basso) - - - - + + Search filters + Filtri di ricerca + + + + Unread + Non letti + + + + In progress + In corso + + + + Highly rated + Con valutazione alta + + + + Recently added + Aggiunti di recente + + + + Search syntax… + Sintassi di ricerca… + + + + + + Set type Imposta il tipo - + + A repair of this library is already running (%1). Wait for it to finish. + È già in corso una riparazione di questa libreria (%1). Attendere il completamento. + + + + The library is locked by a repair that did not finish. + La libreria è bloccata da una riparazione non completata. + + + + The library is locked by a repair started by %1. + La libreria è bloccata da una riparazione avviata da %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Se sei sicuro che non sia in corso nessun'altra riparazione, il blocco può essere rimosso. Rimuovere il blocco e continuare? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Recupero del ripristino non riuscito + + + Set custom cover Imposta la copertina personalizzata - + Delete custom cover Elimina la copertina personalizzata - + Error Errore - + Error opening comic with third party reader. Errore nell'apertura del fumetto con un lettore di terze parti. - + + + YACReader library database (*.ydb) + Database della libreria YACReader (*.ydb) + + + + The library database backup was created at: +%1 + Il backup del database della libreria è stato creato in: +%1 + + + + Unable to create the library database backup: +%1 + Impossibile creare il backup del database della libreria: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Chiudi YACReaderLibraryServer e qualsiasi altra applicazione YACReader che usa questa libreria prima del ripristino. Continuare? + + + + Restoring library database... + Ripristino del database della libreria... + + + + The current library database is invalid. Restore the selected backup anyway? + Il database attuale della libreria non è valido. Ripristinare comunque il backup selezionato? + + + + + The library maintenance lock may be stale. Remove it and retry? + Il blocco di manutenzione della libreria potrebbe essere obsoleto. Rimuoverlo e riprovare? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Riavvia YACReaderLibrary prima di tentare nuovamente il recupero. + + + + The library database was restored successfully. Update the library now? + Il database della libreria è stato ripristinato correttamente. Aggiornare la libreria ora? + + + + Library database damaged + Database della libreria danneggiato + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + Il database della libreria '%1' è danneggiato, quindi gli aggiornamenti, la manutenzione e i backup normali non sono disponibili. YACReader può tentare di riparare il database. Alcuni dati danneggiati potrebbero non essere recuperabili. I backup esistenti non verranno modificati. + + + + Attempt repair + Tenta la riparazione + + + + Restore a backup... + Ripristina un backup... + + + + Repairing library database... + Riparazione del database della libreria... + + + + + + Library database repair + Riparazione del database della libreria + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Un'altra operazione di manutenzione sta usando questa libreria. Riprova al termine. + + + + The library database is already valid. + Il database della libreria è già valido. + + + + Library database repaired + Database della libreria riparato + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + Il database della libreria è stato riparato ricostruendone gli indici. L'originale danneggiato è stato conservato in: +%1 + + + + Library database rebuilt + Database della libreria ricostruito + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + Il database della libreria è stato ricostruito correttamente. L'originale danneggiato è stato conservato in: +%1 + +Aggiornare la libreria ora? + + + + + +The damaged original was preserved at: +%1 + + +L'originale danneggiato è stato conservato in: +%1 + + + + Library database repair failed + Riparazione del database della libreria non riuscita + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + Impossibile riparare il database della libreria: +%1%2 + +Puoi ripristinare un backup dal menu Libreria o ricreare la libreria. + + + library? Libreria? - + Are you sure? Sei sicuro? - + Rescan library for XML info Eseguire nuovamente la scansione della libreria per informazioni XML - + Upgrade failed Aggiornamento non riuscito - + There were errors during library upgrade in: Si sono verificati errori durante l'aggiornamento della libreria in: - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader non trovato. YACReader deve essere installato nella stessa cartella di YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader non trovato. Potrebbe esserci un problema con l'installazione di YACReader. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Riparati: %1 +Non riusciti: %2 +File mancanti: %3 + LibraryWindowActions @@ -1261,384 +1506,425 @@ YACReader non ti fermerà dal creare altre librerie ma è meglio se terrai il nu + Back up library database + Esegui il backup del database della libreria + + + + Create a backup of the current library database + Crea un backup del database attuale della libreria + + + + Restore library database backup + Ripristina il backup del database della libreria + + + + Restore the current library database from a backup + Ripristina il database attuale della libreria da un backup + + + + + Repair covers and comic info + Ripara copertine e informazioni dei fumetti + + + + Retry comics with missing covers or incomplete information + Riprova i fumetti con copertine mancanti o informazioni incomplete + + + Rename library Rinomina la libreria - + Rename current library Rinomina la libreria corrente - + Remove library Rimuovi la libreria - + Remove current library from your collection Rimuovi la libreria corrente dalla tua collezione - + Rescan library for XML info Eseguire nuovamente la scansione della libreria per informazioni XML - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Cerca di trovare informazioni XML incorporate nei file dei fumetti. Devi farlo solo se la libreria è stata creata con la versione 9.8.2 o precedente o se utilizzi software di terze parti per incorporare informazioni XML nei file. - + + Open library folder... + Apri la cartella della libreria... + + + + Open the root folder of the current library + Apri la cartella principale della libreria corrente + + + Show library info Mostra informazioni sulla biblioteca - + Show information about the current library Mostra informazioni sulla libreria corrente - + Open current comic Apri il fumetto corrente - + Open current comic on YACReader Apri il fumetto corrente con YACReader - + Save selected covers to... Salva le copertine selezionate in... - + Save covers of the selected comics as JPG files Salva le copertine dei fumetti selezionati come file JPG - - + + Set as read Setta come letto - + Set comic as read Setta il fumetto come letto - - + + Set as unread Setta come non letto - + Set comic as unread Setta il fumetto come non letto - - + + manga Manga - + Set issue as manga Imposta il problema come manga - - + + comic comico - + Set issue as normal Imposta il problema come normale - + western manga manga occidentali - + Set issue as western manga Imposta il problema come manga occidentale - - + + web comic fumetto web - + Set issue as web comic Imposta il problema come fumetto web - - + + yonkoma Yonkoma - + Set issue as yonkoma Imposta il problema come Yonkoma - + Show/Hide marks Mostra/Nascondi - + Show or hide read marks Mostra o nascondi lo stato di lettura - + Show/Hide recent indicator Mostra/Nascondi l'indicatore recente - + Show or hide recent indicator Mostra o nascondi l'indicatore recente - - + + Fullscreen mode on/off Modalità a schermo interno on/off - + Help, About YACReader Aiuto, Crediti YACReader - + Add new folder Aggiungi una nuova cartella - + Add new folder to the current library Aggiungi una nuova cartella alla libreria corrente - + Delete folder Cancella Cartella - + Delete current folder from disk Cancella la cartella corrente dal disco - + Select root node Seleziona il nodo principale - + Expand all nodes Espandi tutti i nodi - + Collapse all nodes Compatta tutti i nodi - + Show options dialog Mostra le opzioni - + Show comics server options dialog Mostra le opzioni per il server dei fumetti - - + + Change between comics views Cambia tra i modi di visualizzazione dei fumetti - + Open folder... Apri Cartella... - + Set as uncompleted Segna come non completo - + Set as completed Segna come completo - + Set custom cover Imposta la copertina personalizzata - + Delete custom cover Elimina la copertina personalizzata - + western manga (left to right) manga occidentale (da sinistra a destra) - + Open containing folder... Apri la cartella dei contenuti... - + Reset comic rating Resetta la valutazione dei fumetti - + Select all comics Seleziona tutti i fumetti - + Edit Edita - + Assign current order to comics Assegna l'ordinamento corrente ai fumetti - + Update cover Aggiorna copertina - + Delete selected comics Cancella i fumetti selezionati - + Delete metadata from selected comics Elimina i metadati dai fumetti selezionati - + Download tags from Comic Vine Scarica i Tag da Comic Vine - + Focus search line Mettere a fuoco la linea di ricerca - + Focus comics view Focus sulla visualizzazione dei fumetti - + Edit shortcuts Edita scorciatoie - + &Quit &Esci - + Update folder Aggiorna Cartella - + Update current folder Aggiorna la cartella corrente - + Scan legacy XML metadata Scansione dei metadati XML legacy - + Add new reading list Aggiorna la lista di lettura - + Add a new reading list to the current library Aggiungi una lista di lettura alla libreria corrente - + Remove reading list Rimuovi la lista di lettura - + Remove current reading list from the library Rimuovi la lista di lettura dalla libreria - + Add new label Aggiungi una nuova etichetta - + Add a new label to this library Aggiungi una nuova etichetta a questa libreria - + Rename selected list Rinomina la lista selezionata - + Rename any selected labels or lists Rinomina qualsiasi etichetta o lista selezionata - + Add to... Aggiungi a... - + Favorites Favoriti - + Add selected comics to favorites list Aggiungi i fumetti selezionati alla lista dei favoriti @@ -1705,17 +1991,17 @@ YACReader non ti fermerà dal creare altre librerie ma è meglio se terrai il nu Abilita l'immagine di sfondo - + Options Opzioni - + Comic Vine API key API di ComicVine - + Edit Comic Vine API key Edita l'API di ComicVine @@ -1756,63 +2042,63 @@ YACReader non ti fermerà dal creare altre librerie ma è meglio se terrai il nu Aspetto - + Language Lingua - + Application language Lingua dell'applicazione - + System default Predefinita del sistema - + Tray icon settings (experimental) Impostazioni dell'icona nella barra delle applicazioni (sperimentale) - + Close to tray Vicino al vassoio - + Start into the system tray Inizia nella barra delle applicazioni - + ComicInfo.xml legacy support Supporto legacy ComicInfo.xml - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importa metadati da ComicInfo.xml quando aggiungi nuovi fumetti - + Consider 'recent' items added or updated since X days ago Considera gli elementi "recenti" aggiunti o aggiornati da X giorni fa - + Third party reader Lettore di terze parti - + Write {comic_file_path} where the path should go in the command Scrivi {comic_file_path} dove dovrebbe andare il percorso nel comando - + Clear Cancella @@ -1921,7 +2207,7 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam Continua a leggere - + Restart is needed Riavvio Necessario @@ -2291,6 +2577,279 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam Numero di volumi trovati: %1 + + SearchFieldRegistry + + + Text, quoted text + Testo, testo tra virgolette + + + + Integer + Intero + + + + Boolean (true / false) + Booleano (true / false) + + + + Integer (number of days) + Intero (numero di giorni) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Enumerazione (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Titolo del fumetto + + + + Series name + Nome della serie + + + + Issue number + Numero dell'albo + + + + Volume identifier + Identificatore del volume + + + + Reading format + Formato di lettura + + + + Comic rating + Valutazione del fumetto + + + + Textual tags + Tag testuali + + + + Writer credit + Credito dello sceneggiatore + + + + Penciller credit + Credito del disegnatore + + + + Inker credit + Credito dell'inchiostratore + + + + Colorist credit + Credito del colorista + + + + Letterer credit + Credito del letterista + + + + Cover artist credit + Credito dell'artista di copertina + + + + Editor credit + Credito dell'editor + + + + Story arc name + Nome dell'arco narrativo + + + + Position within a story arc + Posizione nell'arco narrativo + + + + Number of issues in a story arc + Numero di albi nell'arco narrativo + + + + Characters appearing in the comic + Personaggi presenti nel fumetto + + + + Teams appearing in the comic + Squadre presenti nel fumetto + + + + Locations appearing in the comic + Luoghi presenti nel fumetto + + + + Primary character or team + Personaggio o squadra principale + + + + Comic synopsis + Sinossi del fumetto + + + + Publisher name + Nome dell'editore + + + + Publishing imprint + Marchio editoriale + + + + Publication format + Formato di pubblicazione + + + + Recommended age rating + Classificazione per età consigliata + + + + Comic genre + Genere del fumetto + + + + ISO language code + Codice lingua ISO + + + + Publication date metadata + Metadati della data di pubblicazione + + + + Series grouping metadata + Metadati di raggruppamento delle serie + + + + Alternate series name + Nome alternativo della serie + + + + Alternate issue number + Numero alternativo dell'albo + + + + Alternate series issue count + Numero di albi della serie alternativa + + + + Number of issues in the series + Numero di albi nella serie + + + + Whether the comic is marked as read + Se il fumetto è contrassegnato come letto + + + + Whether reading has started + Se la lettura è iniziata + + + + Whether metadata has been edited + Se i metadati sono stati modificati + + + + Whether the comic is in color + Se il fumetto è a colori + + + + Number of pages + Numero di pagine + + + + Comic file name + Nome del file del fumetto + + + + When the item was added + Quando l'elemento è stato aggiunto + + + + When the comic was last opened + Quando il fumetto è stato aperto l'ultima volta + + + + Comic notes + Note del fumetto + + + + Review text + Testo della recensione + + + + Parent folder name + Nome della cartella superiore + + + + Default reading format for the folder + Formato di lettura predefinito della cartella + + + + Whether the folder is complete + Se la cartella è completa + + + + Whether the folder is marked as finished + Se la cartella è contrassegnata come terminata + + + + When the folder was updated + Quando la cartella è stata aggiornata + + SearchSingleComic @@ -2310,6 +2869,301 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam Utilizza la ricerca con corrispondenza esatta. Disabilitare se si desidera trovare volumi che corrispondono ad alcune delle parole nel nome. + + SearchSyntaxDialog + + + Common + Comuni + + + + Credits + Crediti + + + + Story + Storia + + + + Publication + Pubblicazione + + + + Reading & files + Lettura e file + + + + Folders + Cartelle + + + + Search syntax + Sintassi di ricerca + + + + Search every comic and folder field, or build precise queries. + Cerca in tutti i campi dei fumetti e delle cartelle oppure crea query precise. + + + + Quick guide + Guida rapida + + + + Fields (%1) + Campi (%1) + + + + Examples + Esempi + + + + Start with a simple search + Inizia con una ricerca semplice + + + + Just start typing. Plain text search across all metadata. + Inizia a digitare. Il testo semplice viene cercato in tutti i metadati. + + + + 1. Search everywhere + 1. Cerca ovunque + + + + Type any text or quoted text. + Digita un testo qualsiasi o un testo tra virgolette. + + + + 2. Target a field + 2. Scegli un campo + + + + Use a field name followed by : or = + Usa il nome di un campo seguito da : oppure = + + + + 3. Combine conditions + 3. Combina le condizioni + + + + Use AND, OR, NOT and parentheses. + Usa AND, OR, NOT e le parentesi. + + + + Operators + Operatori + + + + : or = + : oppure = + + + + contains the text + contiene il testo + + + + matches the complete value + corrisponde al valore completo + + + + greater than / at least + maggiore di / almeno + + + + less than / at most + minore di / al massimo + + + + "quoted text" + "testo tra virgolette" + + + + keeps spaces inside one value + mantiene gli spazi all'interno di un unico valore + + + + Dates and grouping + Date e raggruppamento + + + + added in the last 7 days + aggiunto negli ultimi 7 giorni + + + + added more than 30 days ago + aggiunto più di 30 giorni fa + + + + group alternatives + raggruppa le alternative + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Suggerimenti: gli spazi funzionano come AND e le ricerche non distinguono tra maiuscole e minuscole. Usa le virgolette per includere spazi in un valore. + + + + Find a field… + Trova un campo… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Il testo può essere inserito direttamente o tra virgolette. I campi interi supportano <, <=, > e >=. Nei campi data, l'intero è un numero di giorni (added>7 significa aggiunto negli ultimi 7 giorni). + + + + Field + Campo + + + + Description + Descrizione + + + + Input + Input + + + + Example + Esempio + + + + Examples show the pattern—replace the values with your own. + Gli esempi mostrano lo schema: sostituisci i valori con i tuoi. + + + + Query + Query + + + + What it finds + Cosa trova + + + + Common filters + Filtri comuni + + + + Unread comics + Fumetti non letti + + + + Comics in progress + Fumetti in corso di lettura + + + + Highly rated comics + Fumetti con valutazione alta + + + + Comics added in the last 7 days + Fumetti aggiunti negli ultimi 7 giorni + + + + Metadata + Metadati + + + + Search by series + Cerca per serie + + + + Search by writer + Cerca per sceneggiatore + + + + Manga comics + Fumetti manga + + + + Search textual tags + Cerca nei tag testuali + + + + Advanced combinations + Combinazioni avanzate + + + + Match either writer + Corrisponde a uno dei due sceneggiatori + + + + Group alternatives + Raggruppa le alternative + + + + Exclude a value + Escludi un valore + + + + Older, highly rated comics + Fumetti meno recenti con valutazione alta + + + + Copy query + Copia query + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Gli spazi funzionano come AND. Usa le virgolette per le frasi e le parentesi per controllare il raggruppamento. + + SearchVolume @@ -2415,39 +3269,81 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam ServerConfigDialog - + + + Server connectivity + Connettività del server + + + + Scan to connect + Scansiona per connetterti + + + + Devices on this network can reach your library at the address below. + I dispositivi su questa rete possono accedere alla tua libreria all’indirizzo riportato di seguito. + + + + IP address + Indirizzo IP + + + Port Porta - - enable the server + + Web interface + Interfaccia web + + + + Copy link + Copia link + + + + Open web UI + Apri interfaccia web + + + + Enable the server Abilita il server - + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader è disponibile per iOS e Android. Scoprilo per <a href='https://ios.yacreader.com'>iOS</a> o <a href='https://android.yacreader.com'>Android</a>. + + + enable the server + Abilita il server + + Server connectivity information - Informazioni sulla connettività del server + Informazioni sulla connettività del server - Scan it! - Scansiona! + Scansiona! - - set port - Configura porta + + Set port + set port + Imposta porta - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader è disponibile per dispositivi iOS e Android.<br/>Scoprilo per <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> o <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader è disponibile per dispositivi iOS e Android.<br/>Scoprilo per <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> o <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Scegli un indirizzo IP + Scegli un indirizzo IP @@ -2866,14 +3762,14 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam YACReader::WhatsNewDialog - + Release notes are not available. - + Le note di rilascio non sono disponibili. - + Previous versions - + Versioni precedenti @@ -2909,22 +3805,32 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam YACReaderOptionsDialog - + Save Salva - + Cancel Cancella - + Shortcuts Scorciatoie - + + Keyboard shortcuts + Scorciatoie da tastiera + + + + Customize the keyboard shortcuts used by the application. + Personalizza le scorciatoie da tastiera utilizzate dall'applicazione. + + + Edit shortcuts Modifica scorciatoia @@ -2932,7 +3838,12 @@ Per interrompere un aggiornamento automatico, tocca l'indicatore di caricam YACReaderSearchLineEdit - + + Search filters + Filtri di ricerca + + + type to search Digita per cercare diff --git a/YACReaderLibrary/yacreaderlibrary_ko.ts b/YACReaderLibrary/yacreaderlibrary_ko.ts index 26a650e89..f0f8e8376 100644 --- a/YACReaderLibrary/yacreaderlibrary_ko.ts +++ b/YACReaderLibrary/yacreaderlibrary_ko.ts @@ -166,24 +166,24 @@ 현재 테마 JSON을 불러올 수 없습니다. - + Import theme 테마 가져오기 - + JSON files (*.json);;All files (*) JSON 파일 (*.json);;모든 파일 (*) - + Could not import theme from: %1 다음에서 테마를 가져올 수 없습니다: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed 가져오기 실패 @@ -293,67 +293,67 @@ ComicModel - + yes - + no 아니오 - + Title 제목 - + File Name 파일 이름 - + Pages 페이지 - + Size 크기 - + Read 읽음 - + Current Page 현재 페이지 - + Publication Date 출판일 - + Rating 평점 - + Series 시리즈 - + Volume 볼륨 - + Story Arc 스토리 아크 @@ -617,25 +617,30 @@ FileComic - + 7z not found 7z를 찾을 수 없습니다 - + CRC error on page (%1): some of the pages will not be displayed correctly %1번 페이지에서 CRC 오류 발생: 일부 페이지가 올바르게 표시되지 않을 수 있습니다 - + Unknown error opening the file 파일을 여는 중 알 수 없는 오류가 발생했습니다 - + Format not supported 지원하지 않는 형식입니다 + + + Unsupported EPUB: %1 + + FolderContentView @@ -656,20 +661,25 @@ HelpAboutDialog - + About 정보 - + Help 도움말 - + System info 시스템 정보 + + + Changelog + 변경 로그 + ImportComicsInfoDialog @@ -789,280 +799,350 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>현재 라이브러리에서 레거시 XML 메타데이터 정보를 스캔하고 있습니다.</p><p>이 작업은 한 번만 필요하며, 라이브러리가 YACReaderLibrary 9.8.2 또는 이전 버전으로 만들어진 경우에만 해당됩니다.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>현재 라이브러리에서 누락된 표지와 불완전한 만화 정보를 확인하고 있습니다.</p><p>몇 분 정도 걸릴 수 있습니다. 작업을 중지하고 나중에 다시 실행할 수 있습니다.</p> + LibraryWindow - + Library 라이브러리 - + Open folder... 폴더 열기... - - - + + + western manga (left to right) 서양 만화 (왼쪽 → 오른쪽) - - - + + + 4koma (top to botom) 4koma (top to botom 4컷 (위 → 아래) - + Do you want remove 다음을 제거하시겠습니까: - + YACReader Library YACReader Library - - - + + + manga 망가 - - - + + + comic 만화 - + Are you sure? 확실합니까? - + Rescan library for XML info XML 정보로 라이브러리 재검색 - + Set as read 읽음으로 표시 - - + + Set as unread 읽지 않음으로 표시 - - - + + + web comic 웹 만화 - + Add new folder 새 폴더 추가 - + Delete folder 폴더 삭제 - + Set as uncompleted 미완료로 표시 - + Set as completed 완료로 표시 - + Update folder 폴더 업데이트 - + Folder 폴더 - + Comic 만화 - + Upgrade failed 업그레이드 실패 - + There were errors during library upgrade in: 라이브러리 업그레이드 중 오류 발생: - + + Restore recovery failed + 복원 복구 실패 + + + Update needed 업데이트 필요 - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? 이 라이브러리는 YACReaderLibrary의 이전 버전으로 만들어졌습니다. 업데이트가 필요합니다. 지금 업데이트하시겠습니까? - + Download new version 새 버전 내려받기 - + This library was created with a newer version of YACReaderLibrary. Download the new version now? 이 라이브러리는 YACReaderLibrary의 최신 버전으로 만들어졌습니다. 지금 새 버전을 내려받으시겠습니까? - + Library not available 라이브러리를 사용할 수 없습니다 - + Library '%1' is no longer available. Do you want to remove it? '%1' 라이브러리를 더 이상 사용할 수 없습니다. 제거하시겠습니까? - + Old library 오래된 라이브러리 - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? '%1' 라이브러리는 이전 버전의 YACReaderLibrary로 만들어졌습니다. 다시 만들어야 합니다. 지금 만드시겠습니까? - - + + Copying comics... 만화 복사 중... - - + + Moving comics... 만화 이동 중... - + Folder name: 폴더 이름: - + No folder selected 선택된 폴더 없음 - + Please, select a folder first 먼저 폴더를 선택하세요 - + Error in path 경로 오류 - + There was an error accessing the folder's path 폴더 경로에 접근하는 중 오류가 발생했습니다 - + The selected folder and all its contents will be deleted from your disk. Are you sure? 선택한 폴더와 그 안의 모든 내용이 디스크에서 삭제됩니다. 계속하시겠습니까? - - + + Unable to delete 삭제할 수 없음 - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. 선택한 폴더를 삭제하는 중 문제가 발생했습니다. 쓰기 권한을 확인하고, 다른 응용 프로그램이 이 폴더나 안의 파일을 사용 중인지 확인하세요. - + Add new reading lists 새 읽기 목록 추가 - - + + List name: 목록 이름: - + Delete list/label 목록/라벨 삭제 - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? 선택한 항목이 삭제됩니다. 디스크에서 만화나 폴더는 삭제되지 않습니다. 계속하시겠습니까? - + Rename list name 목록 이름 변경 - - - - + + + + Set type 유형 설정 - + + Search filters + 검색 필터 + + + + Unread + 읽지 않음 + + + + In progress + 읽는 중 + + + + Highly rated + 높은 평점 + + + + Recently added + 최근 추가 + + + + Search syntax… + 검색 구문… + + + + A repair of this library is already running (%1). Wait for it to finish. + 이 라이브러리에 대한 복구가 이미 진행 중입니다 (%1). 완료될 때까지 기다려 주세요. + + + + The library is locked by a repair that did not finish. + 라이브러리가 완료되지 않은 복구에 의해 잠겨 있습니다. + + + + The library is locked by a repair started by %1. + 라이브러리가 %1에서 시작한 복구에 의해 잠겨 있습니다. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + 다른 복구가 실행 중이 아니라고 확신하면 잠금을 해제할 수 있습니다. 잠금을 해제하고 계속하시겠습니까? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Set custom cover 사용자 지정 표지 설정 - + Delete custom cover 사용자 지정 표지 삭제 - + Save covers 표지 저장 - + You are adding too many libraries. 라이브러리를 너무 많이 추가하고 있습니다. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1075,136 +1155,301 @@ YACReaderLibrary will not stop you from creating more libraries but you should k YACReaderLibrary는 라이브러리를 더 만드는 것을 막지 않지만, 라이브러리 수는 적게 유지하는 것이 좋습니다. - - + + YACReader not found YACReader를 찾을 수 없음 - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader를 찾을 수 없습니다. YACReader는 YACReaderLibrary와 같은 폴더에 설치되어야 합니다. - + YACReader not found. There might be a problem with your YACReader installation. YACReader를 찾을 수 없습니다. YACReader 설치에 문제가 있을 수 있습니다. - + Error 오류 - + Error opening comic with third party reader. 타사 뷰어로 만화를 여는 중 오류가 발생했습니다. - + Library not found 라이브러리를 찾을 수 없음 - + The selected folder doesn't contain any library. 선택한 폴더에 라이브러리가 없습니다. - + + + YACReader library database (*.ydb) + YACReader 라이브러리 데이터베이스 (*.ydb) + + + + The library database backup was created at: +%1 + 라이브러리 데이터베이스 백업을 다음 위치에 만들었습니다: +%1 + + + + Unable to create the library database backup: +%1 + 라이브러리 데이터베이스 백업을 만들 수 없습니다: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + 복원하기 전에 YACReaderLibraryServer와 이 라이브러리를 사용하는 다른 모든 YACReader 애플리케이션을 종료하세요. 계속하시겠습니까? + + + + Restoring library database... + 라이브러리 데이터베이스 복원 중... + + + + The current library database is invalid. Restore the selected backup anyway? + 현재 라이브러리 데이터베이스가 유효하지 않습니다. 선택한 백업을 그래도 복원하시겠습니까? + + + + + The library maintenance lock may be stale. Remove it and retry? + 라이브러리 유지 관리 잠금이 오래된 것일 수 있습니다. 잠금을 제거하고 다시 시도하시겠습니까? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +복구를 다시 시도하기 전에 YACReaderLibrary를 다시 시작하세요. + + + + The library database was restored successfully. Update the library now? + 라이브러리 데이터베이스를 성공적으로 복원했습니다. 지금 라이브러리를 업데이트하시겠습니까? + + + + Library database damaged + 라이브러리 데이터베이스 손상 + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + '%1' 라이브러리의 데이터베이스가 손상되어 일반 업데이트, 유지 관리 및 백업을 사용할 수 없습니다. YACReader가 데이터베이스 복구를 시도할 수 있습니다. 손상된 일부 데이터는 복구하지 못할 수 있습니다. 기존 백업은 변경되지 않습니다. + + + + Attempt repair + 복구 시도 + + + + Restore a backup... + 백업 복원... + + + + Repairing library database... + 라이브러리 데이터베이스 복구 중... + + + + + + Library database repair + 라이브러리 데이터베이스 복구 + + + + Another maintenance operation is currently using this library. Try again after it finishes. + 현재 다른 유지 관리 작업에서 이 라이브러리를 사용 중입니다. 작업이 끝난 후 다시 시도하세요. + + + + The library database is already valid. + 라이브러리 데이터베이스가 이미 유효합니다. + + + + Library database repaired + 라이브러리 데이터베이스 복구됨 + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + 인덱스를 다시 빌드하여 라이브러리 데이터베이스를 복구했습니다. 손상된 원본은 다음 위치에 보존되었습니다: +%1 + + + + Library database rebuilt + 라이브러리 데이터베이스 재구축됨 + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + 라이브러리 데이터베이스를 성공적으로 재구축했습니다. 손상된 원본은 다음 위치에 보존되었습니다: +%1 + +지금 라이브러리를 업데이트하시겠습니까? + + + + + +The damaged original was preserved at: +%1 + + +손상된 원본은 다음 위치에 보존되었습니다: +%1 + + + + Library database repair failed + 라이브러리 데이터베이스 복구 실패 + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + 라이브러리 데이터베이스를 복구할 수 없습니다: +%1%2 + +라이브러리 메뉴에서 백업을 복원하거나 라이브러리를 다시 만들 수 있습니다. + + + library? 라이브러리? - + + Remove and delete metadata and backups + 메타데이터 및 백업 제거 후 삭제 + + Remove and delete metadata - 제거 및 메타데이터 삭제 + 제거 및 메타데이터 삭제 - + Library info 라이브러리 정보 - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. 선택한 만화를 삭제하는 중 문제가 발생했습니다. 선택한 파일이나 포함된 폴더의 쓰기 권한을 확인하세요. - + Assign comics numbers 만화에 번호 부여 - + Assign numbers starting in: 다음 번호부터 부여: - + Invalid image 잘못된 이미지 - + The selected file is not a valid image. 선택한 파일이 유효한 이미지가 아닙니다. - + Error saving cover 표지 저장 오류 - + There was an error saving the cover image. 표지 이미지를 저장하는 중 오류가 발생했습니다. - + Error creating the library 라이브러리 생성 오류 - + Error updating the library 라이브러리 업데이트 오류 - + Error opening the library 라이브러리 열기 오류 - + Delete comics 만화 삭제 - + All the selected comics will be deleted from your disk. Are you sure? 선택한 만화가 모두 디스크에서 삭제됩니다. 확실합니까? - + Remove comics 만화 제거 - + Comics will only be deleted from the current label/list. Are you sure? 만화가 현재 라벨/목록에서만 삭제됩니다. 확실합니까? - + Library name already exists 라이브러리 이름 중복 - + There is another library with the name '%1'. '%1' 이름의 라이브러리가 이미 있습니다. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + 복구됨: %1 +실패: %2 +누락된 파일: %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary는 라이브러리를 더 만드는 것을 막지 않지만, + Back up library database + 라이브러리 데이터베이스 백업 + + + + Create a backup of the current library database + 현재 라이브러리 데이터베이스의 백업 만들기 + + + + Restore library database backup + 라이브러리 데이터베이스 백업 복원 + + + + Restore the current library database from a backup + 백업에서 현재 라이브러리 데이터베이스 복원 + + + + + Repair covers and comic info + 표지 및 만화 정보 복구 + + + + Retry comics with missing covers or incomplete information + 표지가 없거나 정보가 불완전한 만화를 다시 처리합니다 + + + Rename library 라이브러리 이름 변경 - + Rename current library 현재 라이브러리 이름 변경 - + Remove library 라이브러리 제거 - + Remove current library from your collection 내 컬렉션에서 현재 라이브러리 제거 - + Rescan library for XML info XML 정보로 라이브러리 재검색 - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. 만화 파일에 포함된 XML 정보를 찾으려고 시도합니다. 9.8.2 이하 버전으로 만든 라이브러리이거나 타사 소프트웨어로 파일에 XML 정보를 포함한 경우에만 필요합니다. - + + Open library folder... + 라이브러리 폴더 열기... + + + + Open the root folder of the current library + 현재 라이브러리의 루트 폴더 열기 + + + Show library info 라이브러리 정보 표시 - + Show information about the current library 현재 라이브러리에 대한 정보 표시 - + Open current comic 현재 만화 열기 - + Open current comic on YACReader YACReader에서 현재 만화 열기 - + Save selected covers to... 선택한 표지 저장... - + Save covers of the selected comics as JPG files 선택한 만화의 표지를 JPG 파일로 저장 - - + + Set as read 읽음으로 표시 - + Set comic as read 만화를 읽음으로 표시 - - + + Set as unread 읽지 않음으로 표시 - + Set comic as unread 만화를 읽지 않음으로 표시 - - + + manga 망가 - + Set issue as manga 만화를 망가로 설정 - - + + comic 만화 - + Set issue as normal 만화를 일반으로 설정 - + western manga 서양 만화 - + Set issue as western manga 만화를 서양 만화로 설정 - - + + web comic 웹 만화 - + Set issue as web comic 만화를 웹 만화로 설정 - - + + yonkoma 4컷 만화 - + Set issue as yonkoma 만화를 4컷 만화로 설정 - + Show/Hide marks 읽음 마크 표시/숨김 - + Show or hide read marks 읽음 마크를 표시하거나 숨김 - + Show/Hide recent indicator 신규 표시 표시/숨김 - + Show or hide recent indicator 신규 표시를 표시하거나 숨김 - - + + Fullscreen mode on/off 전체화면 모드 켜기/끄기 - + Help, About YACReader 도움말, YACReader 정보 - + Add new folder 새 폴더 추가 - + Add new folder to the current library 현재 라이브러리에 새 폴더 추가 - + Delete folder 폴더 삭제 - + Delete current folder from disk 현재 폴더를 디스크에서 삭제 - + Select root node 루트 노드 선택 - + Expand all nodes 모든 노드 펼치기 - + Collapse all nodes 모든 노드 접기 - + Show options dialog 환경설정 다이얼로그 표시 - + Show comics server options dialog 만화 서버 환경설정 다이얼로그 표시 - - + + Change between comics views 만화 보기 전환 - + Open folder... 폴더 열기... - + Set as uncompleted 미완료로 표시 - + Set as completed 완료로 표시 - + Set custom cover 사용자 지정 표지 설정 - + Delete custom cover 사용자 지정 표지 삭제 - + western manga (left to right) 서양 만화 (왼쪽 → 오른쪽) - + Open containing folder... 포함된 폴더 열기... - + Reset comic rating 만화 평점 초기화 - + Select all comics 모든 만화 선택 - + Edit 편집 - + Assign current order to comics 만화에 현재 순서 적용 - + Update cover 표지 업데이트 - + Delete selected comics 선택한 만화 삭제 - + Delete metadata from selected comics 선택한 만화에서 메타데이터 삭제 - + Download tags from Comic Vine Comic Vine에서 태그 내려받기 - + Focus search line 검색창으로 이동 - + Focus comics view 만화 보기로 이동 - + Edit shortcuts 단축키 편집 - + &Quit 끝내기(&Q) - + Update folder 폴더 업데이트 - + Update current folder 현재 폴더 업데이트 - + Scan legacy XML metadata 레거시 XML 메타데이터 스캔 - + Add new reading list 새 읽기 목록 추가 - + Add a new reading list to the current library 현재 라이브러리에 새 읽기 목록 추가 - + Remove reading list 읽기 목록 제거 - + Remove current reading list from the library 라이브러리에서 현재 읽기 목록 제거 - + Add new label 새 라벨 추가 - + Add a new label to this library 이 라이브러리에 새 라벨 추가 - + Rename selected list 선택한 목록 이름 변경 - + Rename any selected labels or lists 선택한 라벨이나 목록 이름 변경 - + Add to... 추가... - + Favorites 즐겨찾기 - + Add selected comics to favorites list 선택한 만화를 즐겨찾기 목록에 추가 @@ -1686,73 +1972,73 @@ YACReaderLibrary는 라이브러리를 더 만드는 것을 막지 않지만, OptionsDialog - + Language 언어 - + Application language 응용 프로그램 언어 - + System default 시스템 기본값 - + Tray icon settings (experimental) 트레이 아이콘 설정 (실험적) - + Close to tray 트레이로 최소화 - + Start into the system tray 시스템 트레이에서 시작 - + Edit Comic Vine API key Comic Vine API 키 편집 - + Comic Vine API key Comic Vine API 키 - + ComicInfo.xml legacy support ComicInfo.xml 레거시 지원 - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics 새 만화 추가 시 ComicInfo.xml에서 메타데이터 가져오기 - + Consider 'recent' items added or updated since X days ago X일 전부터 추가되거나 업데이트된 항목을 '최근'으로 간주 - + Third party reader 타사 뷰어 - + Write {comic_file_path} where the path should go in the command 명령어에서 경로가 들어갈 자리에 {comic_file_path}를 입력하세요 - + Clear 지우기 @@ -1917,12 +2203,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 외관 - + Options 환경설정 - + Restart is needed 재시작이 필요합니다 @@ -2292,6 +2578,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 검색된 %1 수 : %2 + + SearchFieldRegistry + + + Text, quoted text + 텍스트, 따옴표로 묶은 텍스트 + + + + Integer + 정수 + + + + Boolean (true / false) + 부울 값 (true / false) + + + + Integer (number of days) + 정수 (일수) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + 열거형 (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + 만화 제목 + + + + Series name + 시리즈 이름 + + + + Issue number + 호 번호 + + + + Volume identifier + 권 식별자 + + + + Reading format + 읽기 형식 + + + + Comic rating + 만화 평점 + + + + Textual tags + 텍스트 태그 + + + + Writer credit + 글 작가 크레딧 + + + + Penciller credit + 연필화 작가 크레딧 + + + + Inker credit + 펜선 작가 크레딧 + + + + Colorist credit + 채색 작가 크레딧 + + + + Letterer credit + 레터링 작가 크레딧 + + + + Cover artist credit + 표지 작가 크레딧 + + + + Editor credit + 편집자 크레딧 + + + + Story arc name + 스토리 아크 이름 + + + + Position within a story arc + 스토리 아크 내 위치 + + + + Number of issues in a story arc + 스토리 아크의 호 수 + + + + Characters appearing in the comic + 만화에 등장하는 캐릭터 + + + + Teams appearing in the comic + 만화에 등장하는 팀 + + + + Locations appearing in the comic + 만화에 등장하는 장소 + + + + Primary character or team + 주요 캐릭터 또는 팀 + + + + Comic synopsis + 만화 줄거리 + + + + Publisher name + 출판사 이름 + + + + Publishing imprint + 출판 브랜드 + + + + Publication format + 출판 형식 + + + + Recommended age rating + 권장 연령 등급 + + + + Comic genre + 만화 장르 + + + + ISO language code + ISO 언어 코드 + + + + Publication date metadata + 출판일 메타데이터 + + + + Series grouping metadata + 시리즈 그룹화 메타데이터 + + + + Alternate series name + 대체 시리즈 이름 + + + + Alternate issue number + 대체 호 번호 + + + + Alternate series issue count + 대체 시리즈의 호 수 + + + + Number of issues in the series + 시리즈의 호 수 + + + + Whether the comic is marked as read + 만화를 읽음으로 표시했는지 여부 + + + + Whether reading has started + 읽기를 시작했는지 여부 + + + + Whether metadata has been edited + 메타데이터를 편집했는지 여부 + + + + Whether the comic is in color + 만화가 컬러인지 여부 + + + + Number of pages + 페이지 수 + + + + Comic file name + 만화 파일 이름 + + + + When the item was added + 항목을 추가한 시점 + + + + When the comic was last opened + 만화를 마지막으로 연 시점 + + + + Comic notes + 만화 메모 + + + + Review text + 리뷰 텍스트 + + + + Parent folder name + 상위 폴더 이름 + + + + Default reading format for the folder + 폴더의 기본 읽기 형식 + + + + Whether the folder is complete + 폴더가 완전한지 여부 + + + + Whether the folder is marked as finished + 폴더를 완료로 표시했는지 여부 + + + + When the folder was updated + 폴더를 업데이트한 시점 + + SearchSingleComic @@ -2311,6 +2870,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 정확히 일치 검색을 사용합니다. 이름의 일부 단어와 일치하는 볼륨을 찾으려면 비활성화하세요. + + SearchSyntaxDialog + + + Common + 일반 + + + + Credits + 제작진 + + + + Story + 스토리 + + + + Publication + 출판 + + + + Reading & files + 읽기 및 파일 + + + + Folders + 폴더 + + + + Search syntax + 검색 구문 + + + + Search every comic and folder field, or build precise queries. + 모든 만화 및 폴더 필드를 검색하거나 정확한 쿼리를 작성합니다. + + + + Quick guide + 빠른 안내 + + + + Fields (%1) + 필드 (%1) + + + + Examples + 예제 + + + + Start with a simple search + 간단한 검색으로 시작 + + + + Just start typing. Plain text search across all metadata. + 바로 입력을 시작하세요. 일반 텍스트는 모든 메타데이터에서 검색됩니다. + + + + 1. Search everywhere + 1. 전체 검색 + + + + Type any text or quoted text. + 텍스트 또는 따옴표로 묶은 텍스트를 입력합니다. + + + + 2. Target a field + 2. 필드 지정 + + + + Use a field name followed by : or = + 필드 이름 뒤에 : 또는 =을 사용합니다 + + + + 3. Combine conditions + 3. 조건 결합 + + + + Use AND, OR, NOT and parentheses. + AND, OR, NOT 및 괄호를 사용합니다. + + + + Operators + 연산자 + + + + : or = + : 또는 = + + + + contains the text + 텍스트를 포함 + + + + matches the complete value + 전체 값과 일치 + + + + greater than / at least + 초과 / 이상 + + + + less than / at most + 미만 / 이하 + + + + "quoted text" + "따옴표로 묶은 텍스트" + + + + keeps spaces inside one value + 공백을 하나의 값 안에 유지 + + + + Dates and grouping + 날짜 및 그룹화 + + + + added in the last 7 days + 최근 7일 이내에 추가됨 + + + + added more than 30 days ago + 30일보다 오래전에 추가됨 + + + + group alternatives + 대체 조건을 그룹화 + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + 팁: 공백은 AND처럼 작동하며 검색은 대소문자를 구분하지 않습니다. 값에 공백을 포함하려면 따옴표를 사용하세요. + + + + Find a field… + 필드 찾기… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + 텍스트는 그대로 또는 따옴표로 묶어 입력할 수 있습니다. 정수 필드는 <, <=, > 및 >=을 지원합니다. 날짜 필드에서 정수는 일수를 뜻합니다(added>7은 최근 7일 이내에 추가됨을 의미). + + + + Field + 필드 + + + + Description + 설명 + + + + Input + 입력 + + + + Example + 예제 + + + + Examples show the pattern—replace the values with your own. + 예제는 패턴을 보여 줍니다. 값을 원하는 값으로 바꾸세요. + + + + Query + 쿼리 + + + + What it finds + 검색 결과 + + + + Common filters + 일반 필터 + + + + Unread comics + 읽지 않은 만화 + + + + Comics in progress + 읽는 중인 만화 + + + + Highly rated comics + 평점이 높은 만화 + + + + Comics added in the last 7 days + 최근 7일 이내에 추가된 만화 + + + + Metadata + 메타데이터 + + + + Search by series + 시리즈로 검색 + + + + Search by writer + 글 작가로 검색 + + + + Manga comics + 일본 만화 + + + + Search textual tags + 텍스트 태그 검색 + + + + Advanced combinations + 고급 조합 + + + + Match either writer + 두 작가 중 하나와 일치 + + + + Group alternatives + 대체 조건 그룹화 + + + + Exclude a value + 값 제외 + + + + Older, highly rated comics + 오래된 고평점 만화 + + + + Copy query + 쿼리 복사 + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + 공백은 AND처럼 작동합니다. 구문에는 따옴표를, 그룹화 제어에는 괄호를 사용하세요. + + SearchVolume @@ -2416,39 +3270,81 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - - set port + + Set port + set port 포트 설정 - Server connectivity information - 서버 연결 정보 + 서버 연결 정보 - Scan it! - 스캔하세요! + 스캔하세요! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader는 iOS와 Android 기기에서도 사용할 수 있습니다.<br/><a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 또는 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>에서 확인하세요. + YACReader는 iOS와 Android 기기에서도 사용할 수 있습니다.<br/><a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 또는 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>에서 확인하세요. - Choose an IP address - IP 주소 선택 + IP 주소 선택 + + + + + Server connectivity + 서버 연결 - + + Scan to connect + 스캔하여 연결 + + + + Devices on this network can reach your library at the address below. + 이 네트워크의 기기는 아래 주소를 통해 라이브러리에 접속할 수 있습니다. + + + + IP address + IP 주소 + + + Port 포트 - + + Web interface + 웹 인터페이스 + + + + Copy link + 링크 복사 + + + + Open web UI + 웹 UI 열기 + + + + Enable the server + 서버 활성화 + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader는 iOS와 Android에서 사용할 수 있습니다. <a href='https://ios.yacreader.com'>iOS</a> 또는 <a href='https://android.yacreader.com'>Android</a>용 앱을 만나 보세요. + + enable the server - 서버 사용 + 서버 사용 @@ -2867,14 +3763,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + - + Previous versions - + @@ -2910,22 +3806,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save 저장 - + Cancel 취소 - + + Keyboard shortcuts + 키보드 단축키 + + + + Customize the keyboard shortcuts used by the application. + 애플리케이션에서 사용하는 키보드 단축키를 사용자 지정합니다. + + + Edit shortcuts 단축키 편집 - + Shortcuts 단축키 @@ -2933,7 +3839,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + 검색 필터 + + + type to search 검색어 입력 diff --git a/YACReaderLibrary/yacreaderlibrary_nl.ts b/YACReaderLibrary/yacreaderlibrary_nl.ts index fc4d8a6ab..6df1eaa3b 100644 --- a/YACReaderLibrary/yacreaderlibrary_nl.ts +++ b/YACReaderLibrary/yacreaderlibrary_nl.ts @@ -166,24 +166,24 @@ De huidige thema-JSON kan niet worden geladen. - + Import theme Thema importeren - + JSON files (*.json);;All files (*) JSON-bestanden (*.json);;Alle bestanden (*) - + Could not import theme from: %1 Kan thema niet importeren uit: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Importeren is mislukt @@ -293,67 +293,67 @@ ComicModel - + no neen - + yes Ja - + Read Gelezen - + Size Grootte(MB) - + Pages Pagina's - + Title Titel - + File Name Bestandsnaam - + Current Page Huidige pagina - + Publication Date Publicatiedatum - + Rating Beoordeling - + Series Serie - + Volume Deel - + Story Arc Verhaalboog @@ -617,25 +617,30 @@ FileComic - + 7z not found 7Z Archiefbestand niet gevonden - + CRC error on page (%1): some of the pages will not be displayed correctly CRC-fout op pagina (%1): sommige pagina's worden niet correct weergegeven - + Unknown error opening the file Onbekende fout bij het openen van het bestand - + Format not supported Formaat niet ondersteund + + + Unsupported EPUB: %1 + + FolderContentView @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Hulp - + System info Systeeminformatie - + + Changelog + Wijzigingslogboek + + + About Over @@ -789,335 +799,404 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>De huidige bibliotheek wordt gescand op oudere XML-metadata-informatie.</p><p>Dit is slechts één keer nodig en alleen als de bibliotheek is ingepakt met YACReaderLibrary 9.8.2 of eerder.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>De huidige bibliotheek wordt gecontroleerd op ontbrekende covers en onvolledige stripinformatie.</p><p>Dit kan enkele minuten duren. Je kunt het proces stoppen en later opnieuw uitvoeren.</p> + LibraryWindow - + The selected folder doesn't contain any library. De geselecteerde map bevat geen bibliotheek. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Deze bibliotheek is gemaakt met een vorige versie van YACReaderLibrary. Het moet worden bijgewerkt. Nu bijwerken? - + Error opening the library Fout bij openen Bibliotheek - Remove and delete metadata - Verwijder metagegevens + Verwijder metagegevens - + Old library Oude Bibliotheek - + Library Bibliotheek - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Deze bibliotheek is gemaakt met een nieuwere versie van YACReaderLibrary. Download de nieuwe versie? - + Library '%1' is no longer available. Do you want to remove it? Bibliotheek ' %1' is niet langer beschikbaar. Wilt u het verwijderen? - + Open folder... Map openen ... - + Do you want remove Wilt u verwijderen - + Error updating the library Fout bij bijwerken Bibliotheek - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? Bibliotheek ' %1' is gemaakt met een oudere versie van YACReaderLibrary. Zij moet opnieuw worden aangemaakt. Wilt u de bibliotheek nu aanmaken? - + Set as read Instellen als gelezen - + Library not available Bibliotheek niet beschikbaar - + YACReader Library YACReader Bibliotheek - + Error creating the library Fout bij aanmaken Bibliotheek - + Update needed Bijwerken is nodig - + Library name already exists Bibliotheek naam bestaat al - + There is another library with the name '%1'. Er is al een bibliotheek met de naam ' %1 '. - + Download new version Nieuwe versie ophalen - + Delete comics Strips verwijderen - + All the selected comics will be deleted from your disk. Are you sure? Alle geselecteerde strips worden verwijderd van uw schijf. Weet u het zeker? - - + + Set as unread Instellen als ongelezen - + Library not found Bibliotheek niet gevonden - - - + + + manga Manga - - - + + + comic grappig - - - + + + western manga (left to right) westerse manga (van links naar rechts) - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (van boven naar beneden) - + library? Bibliotheek? - + Are you sure? Weet u het zeker? - + Rescan library for XML info Bibliotheek opnieuw scannen op XML-info - - - + + + web comic web-strip - + Add new folder Nieuwe map toevoegen - + Delete folder Map verwijderen - + Set as uncompleted Ingesteld als onvoltooid - + Set as completed Instellen als voltooid - + Update folder Map bijwerken - + Folder Map - + Comic Grappig - + Upgrade failed Upgrade mislukt - + There were errors during library upgrade in: Er zijn fouten opgetreden tijdens de bibliotheekupgrade in: - - + + Copying comics... Strips kopiëren... - - + + Moving comics... Strips verplaatsen... - + Folder name: Mapnaam: - + No folder selected Geen map geselecteerd - + Please, select a folder first Selecteer eerst een map - + Error in path Fout in pad - + There was an error accessing the folder's path Er is een fout opgetreden bij het verkrijgen van toegang tot het pad van de map - + The selected folder and all its contents will be deleted from your disk. Are you sure? De geselecteerde map en de volledige inhoud ervan worden van uw schijf verwijderd. Weet je het zeker? - - + + Unable to delete Kan niet verwijderen - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Er is een probleem opgetreden bij het verwijderen van de geselecteerde mappen. Controleer of er schrijfrechten zijn en zorg ervoor dat alle toepassingen deze mappen of een van de daarin opgenomen bestanden gebruiken. - + Add new reading lists Voeg nieuwe leeslijsten toe - - + + List name: Lijstnaam: - + Delete list/label Lijst/label verwijderen - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? Het geselecteerde item wordt verwijderd, uw strips of mappen worden NIET van uw schijf verwijderd. Weet je het zeker? - + Rename list name Hernoem de lijstnaam - - - - + + + + Set type Soort instellen - + + Search filters + Zoekfilters + + + + Unread + Ongelezen + + + + In progress + Bezig + + + + Highly rated + Hoog gewaardeerd + + + + Recently added + Onlangs toegevoegd + + + + Search syntax… + Zoeksyntaxis… + + + + A repair of this library is already running (%1). Wait for it to finish. + Er wordt al een herstel van deze bibliotheek uitgevoerd (%1). Wacht tot dit is voltooid. + + + + The library is locked by a repair that did not finish. + De bibliotheek is vergrendeld door een herstel dat niet is voltooid. + + + + The library is locked by a repair started by %1. + De bibliotheek is vergrendeld door een herstel gestart door %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Als u zeker weet dat er geen ander herstel bezig is, kan de vergrendeling worden verwijderd. Vergrendeling verwijderen en doorgaan? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Herstel na onderbroken terugzetting mislukt + + + Set custom cover Aangepaste omslag instellen - + Delete custom cover Aangepaste omslag verwijderen - + Save covers Bewaar hoesjes - + You are adding too many libraries. U voegt te veel bibliotheken toe. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1130,81 +1209,247 @@ Je hebt waarschijnlijk maar één bibliotheek nodig in je stripmap op het hoogst YACReaderLibrary zal u er niet van weerhouden om meer bibliotheken te creëren, maar u moet het aantal bibliotheken laag houden. - - + + YACReader not found YACReader niet gevonden - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader niet gevonden. YACReader moet in dezelfde map worden geïnstalleerd als YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader niet gevonden. Er is mogelijk een probleem met uw YACReader-installatie. - + Error Fout - + Error opening comic with third party reader. Fout bij het openen van een strip met een lezer van een derde partij. - + + + YACReader library database (*.ydb) + YACReader-bibliotheekdatabase (*.ydb) + + + + The library database backup was created at: +%1 + De back-up van de bibliotheekdatabase is gemaakt in: +%1 + + + + Unable to create the library database backup: +%1 + De back-up van de bibliotheekdatabase kon niet worden gemaakt: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Sluit YACReaderLibraryServer en alle andere YACReader-programma's die deze bibliotheek gebruiken voordat je deze herstelt. Doorgaan? + + + + Restoring library database... + Bibliotheekdatabase wordt hersteld... + + + + The current library database is invalid. Restore the selected backup anyway? + De huidige bibliotheekdatabase is ongeldig. De geselecteerde back-up toch herstellen? + + + + + The library maintenance lock may be stale. Remove it and retry? + Het onderhoudsslot van de bibliotheek is mogelijk verouderd. Verwijderen en opnieuw proberen? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Start YACReaderLibrary opnieuw voordat je nogmaals herstel probeert. + + + + The library database was restored successfully. Update the library now? + De bibliotheekdatabase is hersteld. De bibliotheek nu bijwerken? + + + + Library database damaged + Bibliotheekdatabase beschadigd + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + De database van bibliotheek '%1' is beschadigd. Normale updates, onderhoud en back-ups zijn daarom niet beschikbaar. YACReader kan proberen de database te herstellen. Sommige beschadigde gegevens kunnen mogelijk niet worden hersteld. Bestaande back-ups worden niet gewijzigd. + + + + Attempt repair + Herstel proberen + + + + Restore a backup... + Een back-up herstellen... + + + + Repairing library database... + Bibliotheekdatabase wordt hersteld... + + + + + + Library database repair + Bibliotheekdatabase herstellen + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Een andere onderhoudsbewerking gebruikt deze bibliotheek momenteel. Probeer het opnieuw wanneer die is voltooid. + + + + The library database is already valid. + De bibliotheekdatabase is al geldig. + + + + Library database repaired + Bibliotheekdatabase hersteld + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + De bibliotheekdatabase is hersteld door de indexen opnieuw op te bouwen. Het beschadigde origineel is bewaard in: +%1 + + + + Library database rebuilt + Bibliotheekdatabase opnieuw opgebouwd + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + De bibliotheekdatabase is opnieuw opgebouwd. Het beschadigde origineel is bewaard in: +%1 + +De bibliotheek nu bijwerken? + + + + + +The damaged original was preserved at: +%1 + + +Het beschadigde origineel is bewaard in: +%1 + + + + Library database repair failed + Herstel van bibliotheekdatabase mislukt + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + De bibliotheekdatabase kon niet worden hersteld: +%1%2 + +Je kunt een back-up herstellen via het menu Bibliotheek of de bibliotheek opnieuw maken. + + + + Remove and delete metadata and backups + Metagegevens en back-ups verwijderen en wissen + + + Library info Bibliotheekinformatie - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Er is een probleem opgetreden bij het verwijderen van de geselecteerde strips. Controleer of er schrijfrechten zijn voor de geselecteerde bestanden of de map waarin deze zich bevinden. - + Assign comics numbers Wijs stripnummers toe - + Assign numbers starting in: Nummers toewijzen beginnend met: - + Invalid image Ongeldige afbeelding - + The selected file is not a valid image. Het geselecteerde bestand is geen geldige afbeelding. - + Error saving cover Fout bij opslaan van dekking - + There was an error saving the cover image. Er is een fout opgetreden bij het opslaan van de omslagafbeelding. - + Remove comics Verwijder strips - + Comics will only be deleted from the current label/list. Are you sure? Strips worden alleen verwijderd van het huidige label/de huidige lijst. Weet je het zeker? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Hersteld: %1 +Mislukt: %2 +Ontbrekende bestanden: %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary zal u er niet van weerhouden om meer bibliotheken te creëren, + Back up library database + Back-up van bibliotheekdatabase maken + + + + Create a backup of the current library database + Een back-up van de huidige bibliotheekdatabase maken + + + + Restore library database backup + Back-up van bibliotheekdatabase herstellen + + + + Restore the current library database from a backup + De huidige bibliotheekdatabase vanuit een back-up herstellen + + + + + Repair covers and comic info + Covers en stripinformatie herstellen + + + + Retry comics with missing covers or incomplete information + Strips met ontbrekende covers of onvolledige informatie opnieuw verwerken + + + Rename library Bibliotheek hernoemen - + Rename current library Huidige Bibliotheek hernoemen - + Remove library Bibliotheek verwijderen - + Remove current library from your collection De huidige Bibliotheek verwijderen uit uw verzameling - + Rescan library for XML info Bibliotheek opnieuw scannen op XML-info - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Probeert XML-informatie te vinden die is ingebed in stripbestanden. U hoeft dit alleen te doen als de bibliotheek is gemaakt met versie 9.8.2 of eerdere versies of als u software van derden gebruikt om XML-informatie in de bestanden in te sluiten. - + + Open library folder... + Bibliotheekmap openen... + + + + Open the root folder of the current library + De hoofdmap van de huidige bibliotheek openen + + + Show library info Bibliotheekinfo tonen - + Show information about the current library Toon informatie over de huidige bibliotheek - + Open current comic Huidige strip openen - + Open current comic on YACReader Huidige strip openen in YACReader - + Save selected covers to... Geselecteerde omslagen opslaan in... - + Save covers of the selected comics as JPG files Sla covers van de geselecteerde strips op als JPG-bestanden - - + + Set as read Instellen als gelezen - + Set comic as read Strip Instellen als gelezen - - + + Set as unread Instellen als ongelezen - + Set comic as unread Strip Instellen als ongelezen - - + + manga Manga - + Set issue as manga Stel het probleem in als manga - - + + comic grappig - + Set issue as normal Stel het probleem in als normaal - + western manga westerse manga - + Set issue as western manga Stel het probleem in als westerse manga - - + + web comic web-strip - + Set issue as web comic Stel het probleem in als webstrip - - + + yonkoma yokoma - + Set issue as yonkoma Stel het probleem in als yonkoma - + Show/Hide marks Toon/Verberg markeringen - + Show or hide read marks Toon of verberg leesmarkeringen - + Show/Hide recent indicator Recente indicator tonen/verbergen - + Show or hide recent indicator Toon of verberg recente indicator - - + + Fullscreen mode on/off Volledig scherm modus aan/of - + Help, About YACReader Help, Over YACReader - + Add new folder Nieuwe map toevoegen - + Add new folder to the current library Voeg een nieuwe map toe aan de huidige bibliotheek - + Delete folder Map verwijderen - + Delete current folder from disk Verwijder de huidige map van schijf - + Select root node Selecteer de hoofd categorie - + Expand all nodes Alle categorieën uitklappen - + Collapse all nodes Vouw alle knooppunten samen - + Show options dialog Toon opties dialoog - + Show comics server options dialog Toon strips-server opties dialoog - - + + Change between comics views Wisselen tussen stripweergaven - + Open folder... Map openen ... - + Set as uncompleted Ingesteld als onvoltooid - + Set as completed Instellen als voltooid - + Set custom cover Aangepaste omslag instellen - + Delete custom cover Aangepaste omslag verwijderen - + western manga (left to right) westerse manga (van links naar rechts) - + Open containing folder... Open map ... - + Reset comic rating Stripbeoordeling opnieuw instellen - + Select all comics Selecteer alle strips - + Edit Bewerken - + Assign current order to comics Wijs de huidige volgorde toe aan strips - + Update cover Strip omslagen bijwerken - + Delete selected comics Geselecteerde strips verwijderen - + Delete metadata from selected comics Verwijder metadata uit geselecteerde strips - + Download tags from Comic Vine Tags downloaden van Comic Vine - + Focus search line Focus zoeklijn - + Focus comics view Focus stripweergave - + Edit shortcuts Snelkoppelingen bewerken - + &Quit &Afsluiten - + Update folder Map bijwerken - + Update current folder Werk de huidige map bij - + Scan legacy XML metadata Scan oudere XML-metagegevens - + Add new reading list Nieuwe leeslijst toevoegen - + Add a new reading list to the current library Voeg een nieuwe leeslijst toe aan de huidige bibliotheek - + Remove reading list Leeslijst verwijderen - + Remove current reading list from the library Verwijder de huidige leeslijst uit de bibliotheek - + Add new label Nieuw etiket toevoegen - + Add a new label to this library Voeg een nieuw label toe aan deze bibliotheek - + Rename selected list Hernoem de geselecteerde lijst - + Rename any selected labels or lists Hernoem alle geselecteerde labels of lijsten - + Add to... Toevoegen aan... - + Favorites Favorieten - + Add selected comics to favorites list Voeg geselecteerde strips toe aan de favorietenlijst @@ -1691,78 +1977,78 @@ YACReaderLibrary zal u er niet van weerhouden om meer bibliotheken te creëren, Verschijning - + Options Opties - + Language Taal - + Application language Applicatietaal - + System default Standaard van het systeem - + Tray icon settings (experimental) Instellingen voor ladepictogram (experimenteel) - + Close to tray Dicht bij lade - + Start into the system tray Begin in het systeemvak - + Edit Comic Vine API key Bewerk de Comic Vine API-sleutel - + Comic Vine API key Comic Vine API-sleutel - + ComicInfo.xml legacy support ComicInfo.xml verouderde ondersteuning - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importeer metagegevens uit ComicInfo.xml wanneer u nieuwe strips toevoegt - + Consider 'recent' items added or updated since X days ago Overweeg 'recente' items die sinds X dagen geleden zijn toegevoegd of bijgewerkt - + Third party reader Lezer van derden - + Write {comic_file_path} where the path should go in the command Schrijf {comic_file_path} waar het pad naartoe moet in de opdracht - + Clear Duidelijk @@ -1922,7 +2208,7 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de Algemeen - + Restart is needed Herstart is nodig @@ -2292,6 +2578,279 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de Aantal %1 gevonden: %2 + + SearchFieldRegistry + + + Text, quoted text + Tekst, tekst tussen aanhalingstekens + + + + Integer + Geheel getal + + + + Boolean (true / false) + Booleaans (true / false) + + + + Integer (number of days) + Geheel getal (aantal dagen) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Opsomming (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Striptitel + + + + Series name + Serienaam + + + + Issue number + Uitgavenummer + + + + Volume identifier + Volumeaanduiding + + + + Reading format + Leesindeling + + + + Comic rating + Stripwaardering + + + + Textual tags + Tekstlabels + + + + Writer credit + Vermelding van de schrijver + + + + Penciller credit + Vermelding van de tekenaar + + + + Inker credit + Vermelding van de inkter + + + + Colorist credit + Vermelding van de inkleurder + + + + Letterer credit + Vermelding van de letteraar + + + + Cover artist credit + Vermelding van de omslagtekenaar + + + + Editor credit + Vermelding van de redacteur + + + + Story arc name + Naam van de verhaallijn + + + + Position within a story arc + Positie binnen een verhaallijn + + + + Number of issues in a story arc + Aantal uitgaven in een verhaallijn + + + + Characters appearing in the comic + Personages die in de strip voorkomen + + + + Teams appearing in the comic + Teams die in de strip voorkomen + + + + Locations appearing in the comic + Locaties die in de strip voorkomen + + + + Primary character or team + Hoofdpersonage of hoofdteam + + + + Comic synopsis + Samenvatting van de strip + + + + Publisher name + Naam van de uitgever + + + + Publishing imprint + Uitgeversimprint + + + + Publication format + Publicatie-indeling + + + + Recommended age rating + Aanbevolen leeftijdsclassificatie + + + + Comic genre + Stripgenre + + + + ISO language code + ISO-taalcode + + + + Publication date metadata + Metagegevens van de publicatiedatum + + + + Series grouping metadata + Metagegevens voor seriegroepering + + + + Alternate series name + Alternatieve serienaam + + + + Alternate issue number + Alternatief uitgavenummer + + + + Alternate series issue count + Aantal uitgaven in de alternatieve serie + + + + Number of issues in the series + Aantal uitgaven in de serie + + + + Whether the comic is marked as read + Of de strip als gelezen is gemarkeerd + + + + Whether reading has started + Of het lezen is begonnen + + + + Whether metadata has been edited + Of de metagegevens zijn bewerkt + + + + Whether the comic is in color + Of de strip in kleur is + + + + Number of pages + Aantal pagina's + + + + Comic file name + Bestandsnaam van de strip + + + + When the item was added + Wanneer het item is toegevoegd + + + + When the comic was last opened + Wanneer de strip voor het laatst is geopend + + + + Comic notes + Stripnotities + + + + Review text + Recensietekst + + + + Parent folder name + Naam van de bovenliggende map + + + + Default reading format for the folder + Standaard leesindeling voor de map + + + + Whether the folder is complete + Of de map compleet is + + + + Whether the folder is marked as finished + Of de map als voltooid is gemarkeerd + + + + When the folder was updated + Wanneer de map is bijgewerkt + + SearchSingleComic @@ -2311,6 +2870,301 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de Gebruik exacte matchzoekopdrachten. Schakel dit uit als u volumes wilt vinden die overeenkomen met enkele woorden in de naam. + + SearchSyntaxDialog + + + Common + Algemeen + + + + Credits + Medewerkers + + + + Story + Verhaal + + + + Publication + Publicatie + + + + Reading & files + Lezen en bestanden + + + + Folders + Mappen + + + + Search syntax + Zoeksyntaxis + + + + Search every comic and folder field, or build precise queries. + Doorzoek elk veld van strips en mappen of stel nauwkeurige zoekopdrachten samen. + + + + Quick guide + Snelgids + + + + Fields (%1) + Velden (%1) + + + + Examples + Voorbeelden + + + + Start with a simple search + Begin met een eenvoudige zoekopdracht + + + + Just start typing. Plain text search across all metadata. + Begin gewoon te typen. Platte tekst doorzoekt alle metagegevens. + + + + 1. Search everywhere + 1. Overal zoeken + + + + Type any text or quoted text. + Typ tekst of tekst tussen aanhalingstekens. + + + + 2. Target a field + 2. Een veld kiezen + + + + Use a field name followed by : or = + Gebruik een veldnaam gevolgd door : of = + + + + 3. Combine conditions + 3. Voorwaarden combineren + + + + Use AND, OR, NOT and parentheses. + Gebruik AND, OR, NOT en haakjes. + + + + Operators + Operatoren + + + + : or = + : of = + + + + contains the text + bevat de tekst + + + + matches the complete value + komt overeen met de volledige waarde + + + + greater than / at least + groter dan / ten minste + + + + less than / at most + kleiner dan / ten hoogste + + + + "quoted text" + "tekst tussen aanhalingstekens" + + + + keeps spaces inside one value + houdt spaties binnen één waarde + + + + Dates and grouping + Datums en groepering + + + + added in the last 7 days + toegevoegd in de afgelopen 7 dagen + + + + added more than 30 days ago + meer dan 30 dagen geleden toegevoegd + + + + group alternatives + groepeert alternatieven + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Tips: spaties werken als AND en zoekopdrachten zijn niet hoofdlettergevoelig. Gebruik aanhalingstekens om spaties in een waarde op te nemen. + + + + Find a field… + Een veld zoeken… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Tekst kan rechtstreeks of tussen aanhalingstekens worden ingevoerd. Velden met gehele getallen ondersteunen <, <=, > en >=. Bij datumvelden is het gehele getal een aantal dagen (added>7 betekent toegevoegd in de afgelopen 7 dagen). + + + + Field + Veld + + + + Description + Beschrijving + + + + Input + Invoer + + + + Example + Voorbeeld + + + + Examples show the pattern—replace the values with your own. + De voorbeelden tonen het patroon — vervang de waarden door uw eigen waarden. + + + + Query + Zoekopdracht + + + + What it finds + Wat wordt gevonden + + + + Common filters + Veelgebruikte filters + + + + Unread comics + Ongelezen strips + + + + Comics in progress + Strips waarmee u bezig bent + + + + Highly rated comics + Hoog gewaardeerde strips + + + + Comics added in the last 7 days + Strips die in de afgelopen 7 dagen zijn toegevoegd + + + + Metadata + Metagegevens + + + + Search by series + Zoeken op serie + + + + Search by writer + Zoeken op schrijver + + + + Manga comics + Manga + + + + Search textual tags + Tekstlabels doorzoeken + + + + Advanced combinations + Geavanceerde combinaties + + + + Match either writer + Overeenkomen met een van beide schrijvers + + + + Group alternatives + Alternatieven groeperen + + + + Exclude a value + Een waarde uitsluiten + + + + Older, highly rated comics + Oudere, hoog gewaardeerde strips + + + + Copy query + Zoekopdracht kopiëren + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Spaties werken als AND. Gebruik aanhalingstekens voor woordgroepen en haakjes om de groepering te bepalen. + + SearchVolume @@ -2416,39 +3270,81 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de ServerConfigDialog - + + + Server connectivity + Serververbinding + + + + Scan to connect + Scan om verbinding te maken + + + + Devices on this network can reach your library at the address below. + Apparaten op dit netwerk kunnen je bibliotheek bereiken via het onderstaande adres. + + + + IP address + IP-adres + + + Port Poort - + + Web interface + Webinterface + + + + Copy link + Link kopiëren + + + + Open web UI + Webinterface openen + + + + Enable the server + Server inschakelen + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader is beschikbaar voor iOS en Android. Ontdek de app voor <a href='https://ios.yacreader.com'>iOS</a> of <a href='https://android.yacreader.com'>Android</a>. + + enable the server - De server instellen + De server instellen - - set port + + Set port + set port Poort instellen - Server connectivity information - Informatie over serverconnectiviteit + Informatie over serverconnectiviteit - Scan it! - Scan het! + Scan het! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader is beschikbaar voor iOS- en Android-apparaten.<br/>Ontdek het voor <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> of <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader is beschikbaar voor iOS- en Android-apparaten.<br/>Ontdek het voor <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> of <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Kies een IP-adres + Kies een IP-adres @@ -2867,14 +3763,14 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de YACReader::WhatsNewDialog - + Release notes are not available. - + Releaseopmerkingen zijn niet beschikbaar. - + Previous versions - + Vorige versies @@ -2910,22 +3806,32 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de YACReaderOptionsDialog - + Save Bewaar - + Cancel Annuleren - + + Keyboard shortcuts + Sneltoetsen + + + + Customize the keyboard shortcuts used by the application. + Pas de sneltoetsen aan die door de toepassing worden gebruikt. + + + Edit shortcuts Snelkoppelingen bewerken - + Shortcuts Snelkoppelingen @@ -2933,7 +3839,12 @@ Om een ​​automatische update te stoppen, tikt u op de laadindicator naast de YACReaderSearchLineEdit - + + Search filters + Zoekfilters + + + type to search typ om te zoeken diff --git a/YACReaderLibrary/yacreaderlibrary_pt.ts b/YACReaderLibrary/yacreaderlibrary_pt.ts index a9538827a..cd7be4386 100644 --- a/YACReaderLibrary/yacreaderlibrary_pt.ts +++ b/YACReaderLibrary/yacreaderlibrary_pt.ts @@ -166,24 +166,24 @@ O tema atual JSON não pôde ser carregado. - + Import theme Importar tema - + JSON files (*.json);;All files (*) Arquivos JSON (*.json);;Todos os arquivos (*) - + Could not import theme from: %1 Não foi possível importar o tema de: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Falha na importação @@ -293,67 +293,67 @@ ComicModel - + yes sim - + no não - + Title Título - + File Name Nome do arquivo - + Pages Páginas - + Size Tamanho - + Read Ler - + Current Page Página atual - + Publication Date Data de publicação - + Rating Avaliação - + Series Série - + Volume Tomo - + Story Arc Arco de história @@ -617,25 +617,30 @@ FileComic - + 7z not found 7z não encontrado - + CRC error on page (%1): some of the pages will not be displayed correctly Erro CRC na página (%1): algumas páginas não serão exibidas corretamente - + Unknown error opening the file Erro desconhecido ao abrir o arquivo - + Format not supported Formato não suportado + + + Unsupported EPUB: %1 + + FolderContentView @@ -656,20 +661,25 @@ HelpAboutDialog - + About Sobre - + Help Ajuda - + System info Informações do sistema + + + Changelog + Registro de alterações + ImportComicsInfoDialog @@ -789,280 +799,350 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>A biblioteca atual está sendo verificada em busca de informações de metadados XML legados.</p><p>Isso só será necessário uma vez e somente se a biblioteca tiver sido criada com YACReaderLibrary 9.8.2 ou anterior.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>A biblioteca atual está sendo verificada em busca de capas ausentes e informações incompletas dos quadrinhos.</p><p>Isso pode levar vários minutos. Você pode interromper o processo e executá-lo novamente mais tarde.</p> + LibraryWindow - + Library Biblioteca - + Open folder... Abrir pasta... - - - + + + western manga (left to right) mangá ocidental (da esquerda para a direita) - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (de cima para baixo) - + Do you want remove Você deseja remover - + YACReader Library Biblioteca YACReader - - - + + + manga mangá - - - + + + comic cômico - + Are you sure? Você tem certeza? - + Rescan library for XML info Reanalisar biblioteca para informa??es XML - + Set as read Definir como lido - - + + Set as unread Definir como não lido - - - + + + web comic quadrinhos da web - + Add new folder Adicionar nova pasta - + Delete folder Excluir pasta - + Set as uncompleted Definir como incompleto - + Set as completed Definir como concluído - + Update folder Atualizar pasta - + Folder Pasta - + Comic Quadrinhos - + Upgrade failed Falha na atualização - + There were errors during library upgrade in: Ocorreram erros durante a atualização da biblioteca em: - + + Restore recovery failed + Falha na recuperação do restauro + + + Update needed Atualização necessária - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Esta biblioteca foi criada com uma versão anterior do YACReaderLibrary. Ele precisa ser atualizado. Atualizar agora? - + Download new version Baixe a nova versão - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Esta biblioteca foi criada com uma versão mais recente do YACReaderLibrary. Baixe a nova versão agora? - + Library not available Biblioteca não disponível - + Library '%1' is no longer available. Do you want to remove it? A biblioteca '%1' não está mais disponível. Você quer removê-lo? - + Old library Biblioteca antiga - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? A biblioteca '%1' foi criada com uma versão mais antiga do YACReaderLibrary. Deve ser criado novamente. Deseja criar a biblioteca agora? - - + + Copying comics... Copiando quadrinhos... - - + + Moving comics... Quadrinhos em movimento... - + Folder name: Nome da pasta: - + No folder selected Nenhuma pasta selecionada - + Please, select a folder first Por favor, selecione uma pasta primeiro - + Error in path Erro no caminho - + There was an error accessing the folder's path Ocorreu um erro ao acessar o caminho da pasta - + The selected folder and all its contents will be deleted from your disk. Are you sure? A pasta selecionada e todo o seu conteúdo serão excluídos do disco. Tem certeza? - - + + Unable to delete Não foi possível excluir - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Ocorreu um problema ao tentar excluir as pastas selecionadas. Por favor, verifique as permissões de gravação e certifique-se de que algum aplicativo esteja usando essas pastas ou qualquer um dos arquivos contidos. - + Add new reading lists Adicione novas listas de leitura - - + + List name: Nome da lista: - + Delete list/label Excluir lista/rótulo - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? O item selecionado será excluído, seus quadrinhos ou pastas NÃO serão excluídos do disco. Tem certeza? - + Rename list name Renomear nome da lista - - - - + + + + Set type Definir tipo - + + Search filters + Filtros de pesquisa + + + + Unread + Não lidos + + + + In progress + Em andamento + + + + Highly rated + Bem avaliados + + + + Recently added + Adicionados recentemente + + + + Search syntax… + Sintaxe de pesquisa… + + + + A repair of this library is already running (%1). Wait for it to finish. + Uma reparação desta biblioteca já está em execução (%1). Aguarde a conclusão. + + + + The library is locked by a repair that did not finish. + A biblioteca está bloqueada por uma reparação que não terminou. + + + + The library is locked by a repair started by %1. + A biblioteca está bloqueada por uma reparação iniciada por %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Se tem certeza de que nenhuma outra reparação está em execução, o bloqueio pode ser removido. Remover o bloqueio e continuar? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Set custom cover Definir capa personalizada - + Delete custom cover Excluir capa personalizada - + Save covers Salvar capas - + You are adding too many libraries. Você está adicionando muitas bibliotecas. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1075,136 +1155,301 @@ Você provavelmente só precisa de uma biblioteca em sua pasta de quadrinhos de YACReaderLibrary não impedirá você de criar mais bibliotecas, mas você deve manter o número de bibliotecas baixo. - - + + YACReader not found YACReader não encontrado - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader não encontrado. YACReader deve ser instalado na mesma pasta que YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader não encontrado. Pode haver um problema com a instalação do YACReader. - + Error Erro - + Error opening comic with third party reader. Erro ao abrir o quadrinho com leitor de terceiros. - + Library not found Biblioteca não encontrada - + The selected folder doesn't contain any library. A pasta selecionada não contém nenhuma biblioteca. - + + + YACReader library database (*.ydb) + Base de dados da biblioteca YACReader (*.ydb) + + + + The library database backup was created at: +%1 + A cópia de segurança da base de dados da biblioteca foi criada em: +%1 + + + + Unable to create the library database backup: +%1 + Não foi possível criar a cópia de segurança da base de dados da biblioteca: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Feche o YACReaderLibraryServer e qualquer outra aplicação YACReader que esteja a usar esta biblioteca antes de restaurar. Continuar? + + + + Restoring library database... + A restaurar a base de dados da biblioteca... + + + + The current library database is invalid. Restore the selected backup anyway? + A base de dados atual da biblioteca não é válida. Restaurar a cópia de segurança selecionada mesmo assim? + + + + + The library maintenance lock may be stale. Remove it and retry? + O bloqueio de manutenção da biblioteca pode estar obsoleto. Removê-lo e tentar novamente? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Reinicie o YACReaderLibrary antes de tentar novamente a recuperação. + + + + The library database was restored successfully. Update the library now? + A base de dados da biblioteca foi restaurada com êxito. Atualizar a biblioteca agora? + + + + Library database damaged + Base de dados da biblioteca danificada + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + A base de dados da biblioteca '%1' está danificada, pelo que as atualizações, a manutenção e as cópias de segurança normais não estão disponíveis. O YACReader pode tentar reparar a base de dados. Alguns dados danificados poderão não ser recuperados. As cópias de segurança existentes não serão alteradas. + + + + Attempt repair + Tentar reparar + + + + Restore a backup... + Restaurar uma cópia de segurança... + + + + Repairing library database... + A reparar a base de dados da biblioteca... + + + + + + Library database repair + Reparação da base de dados da biblioteca + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Outra operação de manutenção está a usar esta biblioteca. Tente novamente quando terminar. + + + + The library database is already valid. + A base de dados da biblioteca já é válida. + + + + Library database repaired + Base de dados da biblioteca reparada + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + A base de dados da biblioteca foi reparada através da reconstrução dos índices. O original danificado foi preservado em: +%1 + + + + Library database rebuilt + Base de dados da biblioteca reconstruída + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + A base de dados da biblioteca foi reconstruída com êxito. O original danificado foi preservado em: +%1 + +Atualizar a biblioteca agora? + + + + + +The damaged original was preserved at: +%1 + + +O original danificado foi preservado em: +%1 + + + + Library database repair failed + Falha ao reparar a base de dados da biblioteca + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + Não foi possível reparar a base de dados da biblioteca: +%1%2 + +Pode restaurar uma cópia de segurança no menu Biblioteca ou recriar a biblioteca. + + + library? biblioteca? - + + Remove and delete metadata and backups + Remover e eliminar metadados e cópias de segurança + + Remove and delete metadata - Remover e excluir metadados + Remover e excluir metadados - + Library info Informações da biblioteca - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Ocorreu um problema ao tentar excluir os quadrinhos selecionados. Por favor, verifique as permissões de gravação nos arquivos selecionados ou na pasta que os contém. - + Assign comics numbers Atribuir números de quadrinhos - + Assign numbers starting in: Atribua números começando em: - + Invalid image Imagem inválida - + The selected file is not a valid image. O arquivo selecionado não é uma imagem válida. - + Error saving cover Erro ao salvar a capa - + There was an error saving the cover image. Ocorreu um erro ao salvar a imagem da capa. - + Error creating the library Erro ao criar a biblioteca - + Error updating the library Erro ao atualizar a biblioteca - + Error opening the library Erro ao abrir a biblioteca - + Delete comics Excluir quadrinhos - + All the selected comics will be deleted from your disk. Are you sure? Todos os quadrinhos selecionados serão excluídos do seu disco. Tem certeza? - + Remove comics Remover quadrinhos - + Comics will only be deleted from the current label/list. Are you sure? Os quadrinhos serão excluídos apenas do rótulo/lista atual. Tem certeza? - + Library name already exists O nome da biblioteca já existe - + There is another library with the name '%1'. Existe outra biblioteca com o nome '%1'. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Reparados: %1 +Falhas: %2 +Arquivos ausentes: %3 + LibraryWindowActions @@ -1262,384 +1507,425 @@ YACReaderLibrary não impedirá você de criar mais bibliotecas, mas você deve + Back up library database + Criar cópia de segurança da base de dados + + + + Create a backup of the current library database + Criar uma cópia de segurança da base de dados atual da biblioteca + + + + Restore library database backup + Restaurar cópia de segurança da base de dados + + + + Restore the current library database from a backup + Restaurar a base de dados atual da biblioteca a partir de uma cópia de segurança + + + + + Repair covers and comic info + Reparar capas e informações dos quadrinhos + + + + Retry comics with missing covers or incomplete information + Processar novamente quadrinhos com capas ausentes ou informações incompletas + + + Rename library Renomear biblioteca - + Rename current library Renomear biblioteca atual - + Remove library Remover biblioteca - + Remove current library from your collection Remover biblioteca atual da sua coleção - + Rescan library for XML info Reanalisar biblioteca para informa??es XML - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Tenta encontrar informações XML incorporadas em arquivos de quadrinhos. Você só precisa fazer isso se a biblioteca foi criada com versões 9.8.2 ou anteriores ou se você estiver usando software de terceiros para incorporar informações XML nos arquivos. - + + Open library folder... + Abrir pasta da biblioteca... + + + + Open the root folder of the current library + Abrir a pasta raiz da biblioteca atual + + + Show library info Mostrar informa??es da biblioteca - + Show information about the current library Mostrar informações sobre a biblioteca atual - + Open current comic Abrir quadrinho atual - + Open current comic on YACReader Abrir quadrinho atual no YACReader - + Save selected covers to... Salvar capas selecionadas em... - + Save covers of the selected comics as JPG files Salve as capas dos quadrinhos selecionados como arquivos JPG - - + + Set as read Definir como lido - + Set comic as read Definir quadrinhos como lidos - - + + Set as unread Definir como não lido - + Set comic as unread Definir quadrinhos como não lidos - - + + manga mangá - + Set issue as manga Definir problema como mangá - - + + comic cômico - + Set issue as normal Defina o problema como normal - + western manga mangá ocidental - + Set issue as western manga Definir problema como mangá ocidental - - + + web comic quadrinhos da web - + Set issue as web comic Definir o problema como web comic - - + + yonkoma tira yonkoma - + Set issue as yonkoma Definir problema como yonkoma - + Show/Hide marks Mostrar/ocultar marcas - + Show or hide read marks Mostrar ou ocultar marcas de leitura - + Show/Hide recent indicator Mostrar/ocultar indicador recente - + Show or hide recent indicator Mostrar ou ocultar indicador recente - - + + Fullscreen mode on/off Modo tela cheia ativado/desativado - + Help, About YACReader Ajuda, Sobre o YACReader - + Add new folder Adicionar nova pasta - + Add new folder to the current library Adicionar nova pasta à biblioteca atual - + Delete folder Excluir pasta - + Delete current folder from disk Exclua a pasta atual do disco - + Select root node Selecionar raiz - + Expand all nodes Expandir todos - + Collapse all nodes Recolher todos os nós - + Show options dialog Mostrar opções - + Show comics server options dialog Mostrar caixa de diálogo de opções do servidor de quadrinhos - - + + Change between comics views Alterar entre visualizações de quadrinhos - + Open folder... Abrir pasta... - + Set as uncompleted Definir como incompleto - + Set as completed Definir como concluído - + Set custom cover Definir capa personalizada - + Delete custom cover Excluir capa personalizada - + western manga (left to right) mangá ocidental (da esquerda para a direita) - + Open containing folder... Abrir a pasta contendo... - + Reset comic rating Redefinir classificação de quadrinhos - + Select all comics Selecione todos os quadrinhos - + Edit Editar - + Assign current order to comics Atribuir ordem atual aos quadrinhos - + Update cover Atualizar capa - + Delete selected comics Excluir quadrinhos selecionados - + Delete metadata from selected comics Excluir metadados dos quadrinhos selecionados - + Download tags from Comic Vine Baixe tags do Comic Vine - + Focus search line Linha de pesquisa de foco - + Focus comics view Visualização de quadrinhos em foco - + Edit shortcuts Editar atalhos - + &Quit &Qfato - + Update folder Atualizar pasta - + Update current folder Atualizar pasta atual - + Scan legacy XML metadata Digitalize metadados XML legados - + Add new reading list Adicionar nova lista de leitura - + Add a new reading list to the current library Adicione uma nova lista de leitura à biblioteca atual - + Remove reading list Remover lista de leitura - + Remove current reading list from the library Remover lista de leitura atual da biblioteca - + Add new label Adicionar novo rótulo - + Add a new label to this library Adicione um novo rótulo a esta biblioteca - + Rename selected list Renomear lista selecionada - + Rename any selected labels or lists Renomeie quaisquer rótulos ou listas selecionados - + Add to... Adicionar à... - + Favorites Favoritos - + Add selected comics to favorites list Adicione quadrinhos selecionados à lista de favoritos @@ -1686,73 +1972,73 @@ YACReaderLibrary não impedirá você de criar mais bibliotecas, mas você deve OptionsDialog - + Language Idioma - + Application language Idioma do aplicativo - + System default Padrão do sistema - + Tray icon settings (experimental) Configurações do ícone da bandeja (experimental) - + Close to tray Perto da bandeja - + Start into the system tray Comece na bandeja do sistema - + Edit Comic Vine API key Editar chave da API Comic Vine - + Comic Vine API key Chave de API do Comic Vine - + ComicInfo.xml legacy support Suporte legado ComicInfo.xml - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Importe metadados de ComicInfo.xml ao adicionar novos quadrinhos - + Consider 'recent' items added or updated since X days ago Considere itens 'recentes' adicionados ou atualizados há X dias - + Third party reader Leitor de terceiros - + Write {comic_file_path} where the path should go in the command Escreva {comic_file_path} onde o caminho deve ir no comando - + Clear Claro @@ -1917,12 +2203,12 @@ Para interromper uma atualização automática, toque no indicador de carregamen Aparência - + Options Opções - + Restart is needed Reiniciar é necessário @@ -2292,6 +2578,279 @@ Para interromper uma atualização automática, toque no indicador de carregamen Número de %1 encontrado: %2 + + SearchFieldRegistry + + + Text, quoted text + Texto, texto entre aspas + + + + Integer + Inteiro + + + + Boolean (true / false) + Booleano (true / false) + + + + Integer (number of days) + Inteiro (número de dias) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Enumeração (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Título do quadrinho + + + + Series name + Nome da série + + + + Issue number + Número da edição + + + + Volume identifier + Identificador do volume + + + + Reading format + Formato de leitura + + + + Comic rating + Avaliação do quadrinho + + + + Textual tags + Tags de texto + + + + Writer credit + Crédito do roteirista + + + + Penciller credit + Crédito do desenhista + + + + Inker credit + Crédito do arte-finalista + + + + Colorist credit + Crédito do colorista + + + + Letterer credit + Crédito do letrista + + + + Cover artist credit + Crédito do artista da capa + + + + Editor credit + Crédito do editor + + + + Story arc name + Nome do arco da história + + + + Position within a story arc + Posição dentro de um arco da história + + + + Number of issues in a story arc + Número de edições em um arco da história + + + + Characters appearing in the comic + Personagens presentes no quadrinho + + + + Teams appearing in the comic + Equipes presentes no quadrinho + + + + Locations appearing in the comic + Locais presentes no quadrinho + + + + Primary character or team + Personagem ou equipe principal + + + + Comic synopsis + Sinopse do quadrinho + + + + Publisher name + Nome da editora + + + + Publishing imprint + Selo editorial + + + + Publication format + Formato da publicação + + + + Recommended age rating + Classificação etária recomendada + + + + Comic genre + Gênero do quadrinho + + + + ISO language code + Código de idioma ISO + + + + Publication date metadata + Metadados da data de publicação + + + + Series grouping metadata + Metadados de agrupamento de séries + + + + Alternate series name + Nome alternativo da série + + + + Alternate issue number + Número alternativo da edição + + + + Alternate series issue count + Quantidade de edições da série alternativa + + + + Number of issues in the series + Número de edições da série + + + + Whether the comic is marked as read + Se o quadrinho está marcado como lido + + + + Whether reading has started + Se a leitura foi iniciada + + + + Whether metadata has been edited + Se os metadados foram editados + + + + Whether the comic is in color + Se o quadrinho é colorido + + + + Number of pages + Número de páginas + + + + Comic file name + Nome do arquivo do quadrinho + + + + When the item was added + Quando o item foi adicionado + + + + When the comic was last opened + Quando o quadrinho foi aberto pela última vez + + + + Comic notes + Notas do quadrinho + + + + Review text + Texto da resenha + + + + Parent folder name + Nome da pasta principal + + + + Default reading format for the folder + Formato de leitura padrão da pasta + + + + Whether the folder is complete + Se a pasta está completa + + + + Whether the folder is marked as finished + Se a pasta está marcada como finalizada + + + + When the folder was updated + Quando a pasta foi atualizada + + SearchSingleComic @@ -2311,6 +2870,301 @@ Para interromper uma atualização automática, toque no indicador de carregamen Use a pesquisa de correspondência exata. Desative se quiser encontrar volumes que correspondam a algumas das palavras do nome. + + SearchSyntaxDialog + + + Common + Comum + + + + Credits + Créditos + + + + Story + História + + + + Publication + Publicação + + + + Reading & files + Leitura e arquivos + + + + Folders + Pastas + + + + Search syntax + Sintaxe de pesquisa + + + + Search every comic and folder field, or build precise queries. + Pesquise em todos os campos de quadrinhos e pastas ou crie consultas precisas. + + + + Quick guide + Guia rápido + + + + Fields (%1) + Campos (%1) + + + + Examples + Exemplos + + + + Start with a simple search + Comece com uma pesquisa simples + + + + Just start typing. Plain text search across all metadata. + Basta começar a digitar. O texto simples é pesquisado em todos os metadados. + + + + 1. Search everywhere + 1. Pesquisar em todos os lugares + + + + Type any text or quoted text. + Digite qualquer texto ou texto entre aspas. + + + + 2. Target a field + 2. Pesquisar em um campo + + + + Use a field name followed by : or = + Use o nome de um campo seguido por : ou = + + + + 3. Combine conditions + 3. Combinar condições + + + + Use AND, OR, NOT and parentheses. + Use AND, OR, NOT e parênteses. + + + + Operators + Operadores + + + + : or = + : ou = + + + + contains the text + contém o texto + + + + matches the complete value + corresponde ao valor completo + + + + greater than / at least + maior que / no mínimo + + + + less than / at most + menor que / no máximo + + + + "quoted text" + "texto entre aspas" + + + + keeps spaces inside one value + mantém os espaços dentro de um único valor + + + + Dates and grouping + Datas e agrupamento + + + + added in the last 7 days + adicionado nos últimos 7 dias + + + + added more than 30 days ago + adicionado há mais de 30 dias + + + + group alternatives + agrupa alternativas + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Dicas: os espaços funcionam como AND e as pesquisas não diferenciam maiúsculas de minúsculas. Use aspas para incluir espaços em um valor. + + + + Find a field… + Localizar um campo… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + O texto pode ser inserido diretamente ou entre aspas. Campos inteiros aceitam <, <=, > e >=. Nos campos de data, o inteiro é um número de dias (added>7 significa adicionado nos últimos 7 dias). + + + + Field + Campo + + + + Description + Descrição + + + + Input + Entrada + + + + Example + Exemplo + + + + Examples show the pattern—replace the values with your own. + Os exemplos mostram o padrão — substitua os valores pelos seus. + + + + Query + Consulta + + + + What it finds + O que encontra + + + + Common filters + Filtros comuns + + + + Unread comics + Quadrinhos não lidos + + + + Comics in progress + Quadrinhos em andamento + + + + Highly rated comics + Quadrinhos bem avaliados + + + + Comics added in the last 7 days + Quadrinhos adicionados nos últimos 7 dias + + + + Metadata + Metadados + + + + Search by series + Pesquisar por série + + + + Search by writer + Pesquisar por roteirista + + + + Manga comics + Mangás + + + + Search textual tags + Pesquisar tags de texto + + + + Advanced combinations + Combinações avançadas + + + + Match either writer + Corresponder a qualquer roteirista + + + + Group alternatives + Agrupar alternativas + + + + Exclude a value + Excluir um valor + + + + Older, highly rated comics + Quadrinhos antigos e bem avaliados + + + + Copy query + Copiar consulta + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Os espaços funcionam como AND. Use aspas para frases e parênteses para controlar o agrupamento. + + SearchVolume @@ -2416,39 +3270,81 @@ Para interromper uma atualização automática, toque no indicador de carregamen ServerConfigDialog - - set port - definir porta + + Set port + set port + Definir porta - Server connectivity information - Informações de conectividade do servidor + Informações de conectividade do servidor - Scan it! - Digitalize! + Digitalize! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - O YACReader está disponível para dispositivos iOS e Android.<br/>Descubra-o para <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> ou <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + O YACReader está disponível para dispositivos iOS e Android.<br/>Descubra-o para <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> ou <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Escolha um endereço IP + Escolha um endereço IP + + + + + Server connectivity + Conectividade do servidor + + + + Scan to connect + Digitalize para ligar - + + Devices on this network can reach your library at the address below. + Os dispositivos nesta rede podem aceder à sua biblioteca através do endereço abaixo. + + + + IP address + Endereço IP + + + Port Porta - + + Web interface + Interface web + + + + Copy link + Copiar ligação + + + + Open web UI + Abrir interface web + + + + Enable the server + Ativar o servidor + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + O YACReader está disponível para iOS e Android. Descubra-o para <a href='https://ios.yacreader.com'>iOS</a> ou <a href='https://android.yacreader.com'>Android</a>. + + enable the server - habilitar o servidor + habilitar o servidor @@ -2867,14 +3763,14 @@ Para interromper uma atualização automática, toque no indicador de carregamen YACReader::WhatsNewDialog - + Release notes are not available. - + As notas de lançamento não estão disponíveis. - + Previous versions - + Versões anteriores @@ -2910,22 +3806,32 @@ Para interromper uma atualização automática, toque no indicador de carregamen YACReaderOptionsDialog - + Save Salvar - + Cancel Cancelar - + + Keyboard shortcuts + Atalhos de teclado + + + + Customize the keyboard shortcuts used by the application. + Personalize os atalhos de teclado utilizados pela aplicação. + + + Edit shortcuts Editar atalhos - + Shortcuts Atalhos @@ -2933,7 +3839,12 @@ Para interromper uma atualização automática, toque no indicador de carregamen YACReaderSearchLineEdit - + + Search filters + Filtros de pesquisa + + + type to search digite para pesquisar diff --git a/YACReaderLibrary/yacreaderlibrary_ru.ts b/YACReaderLibrary/yacreaderlibrary_ru.ts index 220d36804..fc6cbf292 100644 --- a/YACReaderLibrary/yacreaderlibrary_ru.ts +++ b/YACReaderLibrary/yacreaderlibrary_ru.ts @@ -166,24 +166,24 @@ Не удалось загрузить JSON текущей темы. - + Import theme Импортировать тему - + JSON files (*.json);;All files (*) Файлы JSON (*.json);;Все файлы (*) - + Could not import theme from: %1 Не удалось импортировать тему из: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed Импорт не удался @@ -293,67 +293,67 @@ ComicModel - + no нет - + yes да - + Read Прочитано - + Series Ряд - + Volume Объем - + Story Arc Сюжетная арка - + Size Размер - + Pages Всего страниц - + Title Заголовок - + Current Page Текущая страница - + File Name Имя файла - + Publication Date Дата публикации - + Rating Рейтинг @@ -617,22 +617,27 @@ FileComic - + Format not supported Формат не поддерживается - + 7z not found 7z не найден - + Unknown error opening the file Неизвестная ошибка при открытии файла - + + Unsupported EPUB: %1 + + + + CRC error on page (%1): some of the pages will not be displayed correctly Ошибка контрольной суммы CRC на странице (%1): некоторые страницы будут отображаться неправильно @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Настройки - + System info Информация о системе - + + Changelog + Журнал изменений + + + About О программе @@ -789,165 +799,169 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>Текущая библиотека сканируется на предмет устаревших метаданных XML.</p><p>Это необходимо только один раз и только в том случае, если библиотека была создана с помощью YACReaderLibrary 9.8.2 или более ранней версии.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>Текущая библиотека проверяется на отсутствующие обложки и неполные сведения о комиксах.</p><p>Это может занять несколько минут. Процесс можно остановить и запустить снова позже.</p> + LibraryWindow - + The selected folder doesn't contain any library. Выбранная папка не содержит ни одной библиотеки. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Эта библиотека была создана с предыдущей версией YACReaderLibrary. Она должна быть обновлена. Обновить сейчас? - + Comic Комикс - + Folder name: Имя папки: - + The selected folder and all its contents will be deleted from your disk. Are you sure? Выбранная папка и все ее содержимое будет удалено с вашего жёсткого диска. Вы уверены? - + Error opening the library Ошибка открытия библиотеки - - + + YACReader not found YACReader не найден - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Возникла проблема при удалении выбранных папок. Пожалуйста, проверьте права на запись и убедитесь что другие приложения не используют эти папки или файлы. - + Rename list name Изменить имя списка - Remove and delete metadata - Удаление метаданных + Удаление метаданных - + Old library Библиотека из старой версии YACreader - + Set as completed Отметить как завершено - + There was an error accessing the folder's path Ошибка доступа к пути папки - + Library Библиотека - + Comics will only be deleted from the current label/list. Are you sure? Комиксы будут удалены только из выбранного списка/ярлыка. Вы уверены? - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Эта библиотека была создана новой версией YACReaderLibrary. Скачать новую версию сейчас? - - + + Moving comics... Переместить комиксы... - - + + Copying comics... Скопировать комиксы... - + Library '%1' is no longer available. Do you want to remove it? Библиотека '%1' больше не доступна. Вы хотите удалить ее? - + Open folder... Открыть папку... - + Do you want remove Вы хотите удалить библиотеку - + Set as uncompleted Отметить как не завершено - + Error in path Ошибка в пути - + Error updating the library Ошибка обновления библиотеки - + Folder Папка - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? Выбранные элементы будут удалены, ваши комиксы или папки НЕ БУДУТ удалены с вашего жёсткого диска. Вы уверены? - - + + List name: Имя списка: - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? Библиотека '%1' была создана старой версией YACReaderLibrary. Она должна быть вновь создана. Вы хотите создать библиотеку сейчас? - + Save covers Сохранить обложки - + Add new reading lists Добавить новый список чтения - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -960,250 +974,481 @@ YACReaderLibrary will not stop you from creating more libraries but you should k YACReaderLibrary не помешает вам создать больше библиотек, но вы должны иметь не большое количество библиотек. - + Set as read Отметить как прочитано - + Library info Информация о библиотеке - + Assign comics numbers Порядковый номер - + Please, select a folder first Пожалуйста, сначала выберите папку - + Library not available Библиотека не доступна - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Возникла проблема при удалении выбранных комиксов. Пожалуйста, проверьте права на запись для выбранных файлов или содержащую их папку. - + YACReader Library Библиотека YACReader - + Error creating the library Ошибка создания библиотеки - + You are adding too many libraries. Вы добавляете слишком много библиотек. - + Update folder Обновить папку - + Update needed Необходимо обновление - + Library name already exists Имя папки уже используется - + There is another library with the name '%1'. Уже существует другая папка с именем '%1'. - + Delete folder Удалить папку - + Assign numbers starting in: Назначить порядковый номер начиная с: - + Download new version Загрузить новую версию - + + Remove and delete metadata and backups + Удалить библиотеку, метаданные и резервные копии + + + Invalid image Неверное изображение - + The selected file is not a valid image. Выбранный файл не является допустимым изображением. - + Error saving cover Не удалось сохранить обложку. - + There was an error saving the cover image. Не удалось сохранить изображение обложки. - + Delete comics Удалить комиксы - + Add new folder Добавить новую папку - + Delete list/label Удалить список/ярлык - + No folder selected Ни одна папка не была выбрана - + All the selected comics will be deleted from your disk. Are you sure? Все выбранные комиксы будут удалены с вашего жёсткого диска. Вы уверены? - + Remove comics Убрать комиксы - - + + Set as unread Отметить как не прочитано - + Library not found Библиотека не найдена - - - + + + manga манга - - - + + + comic комикс - - - + + + web comic веб-комикс - - - + + + western manga (left to right) западная манга (слева направо) - - + + Unable to delete Не удалось удалить - - - + + + 4koma (top to botom) 4кома (сверху вниз) - - - - + + Search filters + Фильтры поиска + + + + Unread + Непрочитанные + + + + In progress + В процессе + + + + Highly rated + С высокой оценкой + + + + Recently added + Недавно добавленные + + + + Search syntax… + Синтаксис поиска… + + + + + + Set type Тип установки - + + A repair of this library is already running (%1). Wait for it to finish. + Восстановление этой библиотеки уже выполняется (%1). Дождитесь его завершения. + + + + The library is locked by a repair that did not finish. + Библиотека заблокирована незавершённым восстановлением. + + + + The library is locked by a repair started by %1. + Библиотека заблокирована восстановлением, запущенным %1. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Если вы уверены, что никакое другое восстановление не выполняется, блокировку можно снять. Снять блокировку и продолжить? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Не удалось восстановиться после прерванного восстановления + + + Set custom cover Установить собственную обложку - + Delete custom cover Удалить пользовательскую обложку - + Error Ошибка - + Error opening comic with third party reader. Ошибка при открытии комикса с помощью сторонней программы чтения. - + + + YACReader library database (*.ydb) + База данных библиотеки YACReader (*.ydb) + + + + The library database backup was created at: +%1 + Резервная копия базы данных библиотеки создана здесь: +%1 + + + + Unable to create the library database backup: +%1 + Не удалось создать резервную копию базы данных библиотеки: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Перед восстановлением закройте YACReaderLibraryServer и все другие приложения YACReader, использующие эту библиотеку. Продолжить? + + + + Restoring library database... + Восстановление базы данных библиотеки... + + + + The current library database is invalid. Restore the selected backup anyway? + Текущая база данных библиотеки повреждена. Всё равно восстановить выбранную резервную копию? + + + + + The library maintenance lock may be stale. Remove it and retry? + Файл блокировки обслуживания библиотеки может быть устаревшим. Удалить его и повторить попытку? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Перезапустите YACReaderLibrary перед следующей попыткой восстановления. + + + + The library database was restored successfully. Update the library now? + База данных библиотеки успешно восстановлена. Обновить библиотеку сейчас? + + + + Library database damaged + База данных библиотеки повреждена + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + База данных библиотеки «%1» повреждена, поэтому обычные обновления, обслуживание и резервное копирование недоступны. YACReader может попытаться восстановить базу данных. Некоторые повреждённые данные могут быть утрачены. Существующие резервные копии не будут изменены. + + + + Attempt repair + Попытаться восстановить + + + + Restore a backup... + Восстановить резервную копию... + + + + Repairing library database... + Восстановление базы данных библиотеки... + + + + + + Library database repair + Восстановление базы данных библиотеки + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Сейчас эту библиотеку использует другая операция обслуживания. Повторите попытку после её завершения. + + + + The library database is already valid. + База данных библиотеки уже исправна. + + + + Library database repaired + База данных библиотеки восстановлена + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + База данных библиотеки восстановлена путём перестроения индексов. Повреждённый оригинал сохранён здесь: +%1 + + + + Library database rebuilt + База данных библиотеки перестроена + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + База данных библиотеки успешно перестроена. Повреждённый оригинал сохранён здесь: +%1 + +Обновить библиотеку сейчас? + + + + + +The damaged original was preserved at: +%1 + + +Повреждённый оригинал сохранён здесь: +%1 + + + + Library database repair failed + Не удалось восстановить базу данных библиотеки + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + Не удалось восстановить базу данных библиотеки: +%1%2 + +Можно восстановить резервную копию из меню «Библиотека» или создать библиотеку заново. + + + library? ? - + Are you sure? Вы уверены? - + Rescan library for XML info Повторное сканирование библиотеки для получения информации XML - + Upgrade failed Обновление не удалось - + There were errors during library upgrade in: При обновлении библиотеки возникли ошибки: - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader не найден. YACReader должен быть установлен в ту же папку, что и YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. YACReader не найден. Возможно, возникла проблема с установкой YACReader. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Восстановлено: %1 +Ошибок: %2 +Отсутствующих файлов: %3 + LibraryWindowActions @@ -1261,384 +1506,425 @@ YACReaderLibrary не помешает вам создать больше биб + Back up library database + Создать резервную копию базы данных + + + + Create a backup of the current library database + Создать резервную копию текущей базы данных библиотеки + + + + Restore library database backup + Восстановить резервную копию базы данных + + + + Restore the current library database from a backup + Восстановить текущую базу данных библиотеки из резервной копии + + + + + Repair covers and comic info + Восстановить обложки и сведения о комиксах + + + + Retry comics with missing covers or incomplete information + Повторно обработать комиксы с отсутствующими обложками или неполными сведениями + + + Rename library Переименовать библиотеку - + Rename current library Переименовать эту библиотеку - + Remove library Удалить библиотеку - + Remove current library from your collection Удалить эту библиотеку из своей коллекции - + Rescan library for XML info Повторное сканирование библиотеки для получения информации XML - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Пытается найти информацию XML, встроенную в файлы комиксов. Это необходимо делать только в том случае, если библиотека была создана с помощью версии 9.8.2 или более ранней, или если вы используете стороннее программное обеспечение для встраивания информации XML в файлы. - + + Open library folder... + Открыть папку библиотеки... + + + + Open the root folder of the current library + Открыть корневую папку текущей библиотеки + + + Show library info Показать информацию о библиотеке - + Show information about the current library Показать информацию о текущей библиотеке - + Open current comic Открыть выбранный комикс - + Open current comic on YACReader Открыть комикс в YACReader - + Save selected covers to... Сохранить выбранные обложки в... - + Save covers of the selected comics as JPG files Сохранить обложки выбранных комиксов как JPG файлы - - + + Set as read Отметить как прочитано - + Set comic as read Отметить комикс как прочитано - - + + Set as unread Отметить как не прочитано - + Set comic as unread Отметить комикс как не прочитано - - + + manga манга - + Set issue as manga Установить выпуск как мангу - - + + comic комикс - + Set issue as normal Установите проблему как обычно - + western manga вестерн манга - + Set issue as western manga Установить выпуск как западную мангу - - + + web comic веб-комикс - + Set issue as web comic Установить выпуск как веб-комикс - - + + yonkoma йонкома - + Set issue as yonkoma Установить проблему как йонкома - + Show/Hide marks Показать/Спрятать пометки - + Show or hide read marks Показать или спрятать отметку прочтено - + Show/Hide recent indicator Показать/скрыть индикатор последних событий - + Show or hide recent indicator Показать или скрыть недавний индикатор - - + + Fullscreen mode on/off Полноэкранный режим включить/выключить - + Help, About YACReader О программе - + Add new folder Добавить новую папку - + Add new folder to the current library Добавить новую папку в текущую библиотеку - + Delete folder Удалить папку - + Delete current folder from disk Удалить выбранную папку с жёсткого диска - + Select root node Домашняя папка - + Expand all nodes Раскрыть все папки - + Collapse all nodes Свернуть все папки - + Show options dialog Настройки - + Show comics server options dialog Настройки сервера YACReader - - + + Change between comics views Изменение внешнего вида потока комиксов - + Open folder... Открыть папку... - + Set as uncompleted Отметить как не завершено - + Set as completed Отметить как завершено - + Set custom cover Установить собственную обложку - + Delete custom cover Удалить пользовательскую обложку - + western manga (left to right) западная манга (слева направо) - + Open containing folder... Открыть выбранную папку... - + Reset comic rating Сбросить рейтинг комикса - + Select all comics Выбрать все комиксы - + Edit Редактировать информацию - + Assign current order to comics Назначить порядковый номер - + Update cover Обновить обложки - + Delete selected comics Удалить выбранное - + Delete metadata from selected comics Удалить метаданные из выбранных комиксов - + Download tags from Comic Vine Скачать теги из Comic Vine - + Focus search line Строка поиска фокуса - + Focus comics view Просмотр комиксов в фокусе - + Edit shortcuts Редактировать горячие клавиши - + &Quit &Qкостюм - + Update folder Обновить папку - + Update current folder Обновить выбранную папку - + Scan legacy XML metadata Сканировать устаревшие метаданные XML - + Add new reading list Создать новый список чтения - + Add a new reading list to the current library Создать новый список чтения - + Remove reading list Удалить список чтения - + Remove current reading list from the library Удалить выбранный ярлык/список чтения - + Add new label Создать новый ярлык - + Add a new label to this library Создать новый ярлык - + Rename selected list Переименовать выбранный список - + Rename any selected labels or lists Переименовать выбранный ярлык/список чтения - + Add to... Добавить в... - + Favorites Избранное - + Add selected comics to favorites list Добавить выбранные комиксы в список избранного @@ -1705,17 +1991,17 @@ YACReaderLibrary не помешает вам создать больше биб Включить фоновое изображение - + Options Настройки - + Comic Vine API key Comic Vine API ключ - + Edit Comic Vine API key Редактировать Comic Vine API ключ @@ -1756,63 +2042,63 @@ YACReaderLibrary не помешает вам создать больше биб Появление - + Language Язык - + Application language Язык приложения - + System default Системный по умолчанию - + Tray icon settings (experimental) Настройки значков в трее (экспериментально) - + Close to tray Рядом с лотком - + Start into the system tray Запустите в системном трее - + ComicInfo.xml legacy support Поддержка устаревших версий ComicInfo.xml - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Импортируйте метаданные из ComicInfo.xml при добавлении новых комиксов. - + Consider 'recent' items added or updated since X days ago Учитывайте «недавние» элементы, добавленные или обновленные X дней назад. - + Third party reader Сторонний читатель - + Write {comic_file_path} where the path should go in the command Напишите {comic_file_path}, где должен идти путь в команде. - + Clear Очистить @@ -1921,7 +2207,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Продолжить чтение - + Restart is needed Требуется перезагрузка @@ -2291,6 +2577,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Количество найденных томов : %1 + + SearchFieldRegistry + + + Text, quoted text + Текст, текст в кавычках + + + + Integer + Целое число + + + + Boolean (true / false) + Логическое значение (true / false) + + + + Integer (number of days) + Целое число (количество дней) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Перечисление (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Название комикса + + + + Series name + Название серии + + + + Issue number + Номер выпуска + + + + Volume identifier + Идентификатор тома + + + + Reading format + Формат чтения + + + + Comic rating + Оценка комикса + + + + Textual tags + Текстовые метки + + + + Writer credit + Автор сценария + + + + Penciller credit + Художник-карандашист + + + + Inker credit + Контуровщик + + + + Colorist credit + Колорист + + + + Letterer credit + Леттерер + + + + Cover artist credit + Художник обложки + + + + Editor credit + Редактор + + + + Story arc name + Название сюжетной арки + + + + Position within a story arc + Позиция в сюжетной арке + + + + Number of issues in a story arc + Количество выпусков в сюжетной арке + + + + Characters appearing in the comic + Персонажи, появляющиеся в комиксе + + + + Teams appearing in the comic + Команды, появляющиеся в комиксе + + + + Locations appearing in the comic + Места, появляющиеся в комиксе + + + + Primary character or team + Главный персонаж или команда + + + + Comic synopsis + Краткое содержание комикса + + + + Publisher name + Название издательства + + + + Publishing imprint + Импринт издательства + + + + Publication format + Формат публикации + + + + Recommended age rating + Рекомендуемый возрастной рейтинг + + + + Comic genre + Жанр комикса + + + + ISO language code + Код языка ISO + + + + Publication date metadata + Метаданные даты публикации + + + + Series grouping metadata + Метаданные группировки серий + + + + Alternate series name + Альтернативное название серии + + + + Alternate issue number + Альтернативный номер выпуска + + + + Alternate series issue count + Количество выпусков альтернативной серии + + + + Number of issues in the series + Количество выпусков в серии + + + + Whether the comic is marked as read + Отмечен ли комикс как прочитанный + + + + Whether reading has started + Начато ли чтение + + + + Whether metadata has been edited + Изменялись ли метаданные + + + + Whether the comic is in color + Является ли комикс цветным + + + + Number of pages + Количество страниц + + + + Comic file name + Имя файла комикса + + + + When the item was added + Когда элемент был добавлен + + + + When the comic was last opened + Когда комикс открывался в последний раз + + + + Comic notes + Примечания к комиксу + + + + Review text + Текст рецензии + + + + Parent folder name + Название родительской папки + + + + Default reading format for the folder + Формат чтения папки по умолчанию + + + + Whether the folder is complete + Завершена ли папка + + + + Whether the folder is marked as finished + Отмечена ли папка как законченная + + + + When the folder was updated + Когда папка была обновлена + + SearchSingleComic @@ -2310,6 +2869,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Используйте поиск по точному совпадению. Отключите, если хотите найти тома, соответствующие некоторым словам в названии. + + SearchSyntaxDialog + + + Common + Общие + + + + Credits + Авторы + + + + Story + Сюжет + + + + Publication + Публикация + + + + Reading & files + Чтение и файлы + + + + Folders + Папки + + + + Search syntax + Синтаксис поиска + + + + Search every comic and folder field, or build precise queries. + Ищите во всех полях комиксов и папок или создавайте точные запросы. + + + + Quick guide + Краткое руководство + + + + Fields (%1) + Поля (%1) + + + + Examples + Примеры + + + + Start with a simple search + Начните с простого поиска + + + + Just start typing. Plain text search across all metadata. + Просто начните вводить текст. Обычный текст ищется во всех метаданных. + + + + 1. Search everywhere + 1. Поиск везде + + + + Type any text or quoted text. + Введите любой текст или текст в кавычках. + + + + 2. Target a field + 2. Поиск по полю + + + + Use a field name followed by : or = + Укажите имя поля, а затем : или = + + + + 3. Combine conditions + 3. Объединение условий + + + + Use AND, OR, NOT and parentheses. + Используйте AND, OR, NOT и круглые скобки. + + + + Operators + Операторы + + + + : or = + : или = + + + + contains the text + содержит текст + + + + matches the complete value + полностью совпадает со значением + + + + greater than / at least + больше / не меньше + + + + less than / at most + меньше / не больше + + + + "quoted text" + "текст в кавычках" + + + + keeps spaces inside one value + сохраняет пробелы внутри одного значения + + + + Dates and grouping + Даты и группировка + + + + added in the last 7 days + добавлено за последние 7 дней + + + + added more than 30 days ago + добавлено более 30 дней назад + + + + group alternatives + группирует варианты + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + Советы: пробелы работают как AND, а поиск не учитывает регистр. Используйте кавычки, чтобы включить пробелы в значение. + + + + Find a field… + Найти поле… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Текст можно вводить без кавычек или в кавычках. Целочисленные поля поддерживают <, <=, > и >=. В полях даты целое число означает количество дней (added>7 означает добавленное за последние 7 дней). + + + + Field + Поле + + + + Description + Описание + + + + Input + Ввод + + + + Example + Пример + + + + Examples show the pattern—replace the values with your own. + Примеры показывают шаблон — замените значения своими. + + + + Query + Запрос + + + + What it finds + Что будет найдено + + + + Common filters + Общие фильтры + + + + Unread comics + Непрочитанные комиксы + + + + Comics in progress + Комиксы в процессе чтения + + + + Highly rated comics + Комиксы с высокой оценкой + + + + Comics added in the last 7 days + Комиксы, добавленные за последние 7 дней + + + + Metadata + Метаданные + + + + Search by series + Поиск по серии + + + + Search by writer + Поиск по автору сценария + + + + Manga comics + Манга + + + + Search textual tags + Поиск по текстовым меткам + + + + Advanced combinations + Расширенные комбинации + + + + Match either writer + Совпадение с любым из авторов + + + + Group alternatives + Сгруппировать варианты + + + + Exclude a value + Исключить значение + + + + Older, highly rated comics + Старые комиксы с высокой оценкой + + + + Copy query + Копировать запрос + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Пробелы работают как AND. Используйте кавычки для фраз и круглые скобки для управления группировкой. + + SearchVolume @@ -2415,39 +3269,81 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - + + + Server connectivity + Подключение к серверу + + + + Scan to connect + Отсканируйте для подключения + + + + Devices on this network can reach your library at the address below. + Устройства в этой сети могут получить доступ к вашей библиотеке по указанному ниже адресу. + + + + IP address + IP-адрес + + + Port Порт - + + Web interface + Веб-интерфейс + + + + Copy link + Копировать ссылку + + + + Open web UI + Открыть веб-интерфейс + + + + Enable the server + Включить сервер + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader доступен для iOS и Android. Установите его для <a href='https://ios.yacreader.com'>iOS</a> или <a href='https://android.yacreader.com'>Android</a>. + + enable the server - активировать сервер + активировать сервер - Server connectivity information - Информация о подключении + Информация о подключении - Scan it! - Сканируйте! + Сканируйте! - - set port - указать порт + + Set port + set port + Установить порт - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader доступен для устройств iOS и Android.<br/>Найдите его для <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> или <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader доступен для устройств iOS и Android.<br/>Найдите его для <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> или <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - Choose an IP address - Выбрать IP адрес + Выбрать IP адрес @@ -2866,14 +3762,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + Примечания к выпуску недоступны. - + Previous versions - + Предыдущие версии @@ -2909,22 +3805,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save Сохранить - + Cancel Отмена - + Shortcuts Горячие клавиши - + + Keyboard shortcuts + Сочетания клавиш + + + + Customize the keyboard shortcuts used by the application. + Настройте сочетания клавиш, используемые приложением. + + + Edit shortcuts Редактировать горячие клавиши @@ -2932,7 +3838,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + Фильтры поиска + + + type to search Начать поиск diff --git a/YACReaderLibrary/yacreaderlibrary_source.ts b/YACReaderLibrary/yacreaderlibrary_source.ts index 2b7014544..d9b1cbdc8 100644 --- a/YACReaderLibrary/yacreaderlibrary_source.ts +++ b/YACReaderLibrary/yacreaderlibrary_source.ts @@ -166,23 +166,23 @@ - + Import theme - + JSON files (*.json);;All files (*) - + Could not import theme from: %1 - + Could not import theme from: %1 @@ -190,7 +190,7 @@ - + Import failed @@ -289,67 +289,67 @@ ComicModel - + yes - + no - + Title - + File Name - + Pages - + Size - + Read - + Current Page - + Publication Date - + Rating - + Series - + Volume - + Story Arc @@ -613,25 +613,30 @@ FileComic - + 7z not found - + CRC error on page (%1): some of the pages will not be displayed correctly - + Unknown error opening the file - + Format not supported + + + Unsupported EPUB: %1 + + FolderContentView @@ -652,20 +657,25 @@ HelpAboutDialog - + About - + Help - + System info + + + Changelog + + ImportComicsInfoDialog @@ -785,280 +795,350 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + + LibraryWindow - + Library - + Open folder... - - - + + + western manga (left to right) - - - + + + 4koma (top to botom) 4koma (top to botom - + Do you want remove - + YACReader Library - - - + + + manga - - - + + + comic - + Are you sure? - + Rescan library for XML info - + Set as read - - + + Set as unread - - - + + + web comic - + Add new folder - + Delete folder - + Set as uncompleted - + Set as completed - + Update folder - + Folder - + Comic - + Upgrade failed - + There were errors during library upgrade in: - + + Restore recovery failed + + + + Update needed - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? - + Download new version - + This library was created with a newer version of YACReaderLibrary. Download the new version now? - + Library not available - + Library '%1' is no longer available. Do you want to remove it? - + Old library - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? - - + + Copying comics... - - + + Moving comics... - + Folder name: - + No folder selected - + Please, select a folder first - + Error in path - + There was an error accessing the folder's path - + The selected folder and all its contents will be deleted from your disk. Are you sure? - - + + Unable to delete - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. - + Add new reading lists - - + + List name: - + Delete list/label - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? - + Rename list name - - - - + + + + Set type - + + Search filters + + + + + Unread + + + + + In progress + + + + + Highly rated + + + + + Recently added + + + + + Search syntax… + + + + + A repair of this library is already running (%1). Wait for it to finish. + + + + + The library is locked by a repair that did not finish. + + + + + The library is locked by a repair started by %1. + + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Set custom cover - + Delete custom cover - + Save covers - + You are adding too many libraries. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1067,136 +1147,281 @@ YACReaderLibrary will not stop you from creating more libraries but you should k - - + + YACReader not found - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. - + YACReader not found. There might be a problem with your YACReader installation. - + Error - + Error opening comic with third party reader. - + Library not found - + The selected folder doesn't contain any library. - + + + YACReader library database (*.ydb) + + + + + The library database backup was created at: +%1 + + + + + Unable to create the library database backup: +%1 + + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + + + + + Restoring library database... + + + + + The current library database is invalid. Restore the selected backup anyway? + + + + + + The library maintenance lock may be stale. Remove it and retry? + + + + + + +Restart YACReaderLibrary before attempting recovery again. + + + + + The library database was restored successfully. Update the library now? + + + + + Library database damaged + + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + + + + + Attempt repair + + + + + Restore a backup... + + + + + Repairing library database... + + + + + + + Library database repair + + + + + Another maintenance operation is currently using this library. Try again after it finishes. + + + + + The library database is already valid. + + + + + Library database repaired + + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + + + + + Library database rebuilt + + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + + + + + + +The damaged original was preserved at: +%1 + + + + + Library database repair failed + + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + + + + library? - - Remove and delete metadata + + Remove and delete metadata and backups - + Library info - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. - + Assign comics numbers - + Assign numbers starting in: - + Invalid image - + The selected file is not a valid image. - + Error saving cover - + There was an error saving the cover image. - + Error creating the library - + Error updating the library - + Error opening the library - + Delete comics - + All the selected comics will be deleted from your disk. Are you sure? - + Remove comics - + Comics will only be deleted from the current label/list. Are you sure? - + Library name already exists - + There is another library with the name '%1'. + + + Repaired: %1 +Failed: %2 +Missing files: %3 + + LibraryWindowActions @@ -1254,384 +1479,425 @@ YACReaderLibrary will not stop you from creating more libraries but you should k - Rename library + Back up library database + Create a backup of the current library database + + + + + Restore library database backup + + + + + Restore the current library database from a backup + + + + + + Repair covers and comic info + + + + + Retry comics with missing covers or incomplete information + + + + + Rename library + + + + Rename current library Renomear biblioteca atual - + Remove library - + Remove current library from your collection Remover biblioteca atual da sua coleção - + Rescan library for XML info - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. - + + Open library folder... + + + + + Open the root folder of the current library + + + + Show library info - + Show information about the current library - + Open current comic - + Open current comic on YACReader Abrir quadrinho atual no YACReader - + Save selected covers to... - + Save covers of the selected comics as JPG files - - + + Set as read - + Set comic as read - - + + Set as unread - + Set comic as unread - - + + manga - + Set issue as manga - - + + comic - + Set issue as normal - + western manga - + Set issue as western manga - - + + web comic - + Set issue as web comic - - + + yonkoma - + Set issue as yonkoma - + Show/Hide marks - + Show or hide read marks - + Show/Hide recent indicator - + Show or hide recent indicator - - + + Fullscreen mode on/off - + Help, About YACReader Ajuda, Sobre o YACReader - + Add new folder - + Add new folder to the current library - + Delete folder - + Delete current folder from disk - + Select root node Selecionar raiz - + Expand all nodes Expandir todos - + Collapse all nodes - + Show options dialog Mostrar opções - + Show comics server options dialog - - + + Change between comics views - + Open folder... - + Set as uncompleted - + Set as completed - + Set custom cover - + Delete custom cover - + western manga (left to right) - + Open containing folder... Abrir a pasta contendo... - + Reset comic rating - + Select all comics - + Edit - + Assign current order to comics - + Update cover - + Delete selected comics - + Delete metadata from selected comics - + Download tags from Comic Vine - + Focus search line - + Focus comics view - + Edit shortcuts - + &Quit - + Update folder - + Update current folder - + Scan legacy XML metadata - + Add new reading list - + Add a new reading list to the current library - + Remove reading list - + Remove current reading list from the library - + Add new label - + Add a new label to this library - + Rename selected list - + Rename any selected labels or lists - + Add to... - + Favorites - + Add selected comics to favorites list @@ -1678,73 +1944,73 @@ YACReaderLibrary will not stop you from creating more libraries but you should k OptionsDialog - + Language - + Application language - + System default - + Tray icon settings (experimental) - + Close to tray - + Start into the system tray - + Edit Comic Vine API key - + Comic Vine API key - + ComicInfo.xml legacy support - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics - + Consider 'recent' items added or updated since X days ago - + Third party reader - + Write {comic_file_path} where the path should go in the command - + Clear @@ -1906,12 +2172,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t - + Options - + Restart is needed @@ -2281,6 +2547,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t + + SearchFieldRegistry + + + Text, quoted text + + + + + Integer + + + + + Boolean (true / false) + + + + + Integer (number of days) + + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + + Comic title + + + + + Series name + + + + + Issue number + + + + + Volume identifier + + + + + Reading format + + + + + Comic rating + + + + + Textual tags + + + + + Writer credit + + + + + Penciller credit + + + + + Inker credit + + + + + Colorist credit + + + + + Letterer credit + + + + + Cover artist credit + + + + + Editor credit + + + + + Story arc name + + + + + Position within a story arc + + + + + Number of issues in a story arc + + + + + Characters appearing in the comic + + + + + Teams appearing in the comic + + + + + Locations appearing in the comic + + + + + Primary character or team + + + + + Comic synopsis + + + + + Publisher name + + + + + Publishing imprint + + + + + Publication format + + + + + Recommended age rating + + + + + Comic genre + + + + + ISO language code + + + + + Publication date metadata + + + + + Series grouping metadata + + + + + Alternate series name + + + + + Alternate issue number + + + + + Alternate series issue count + + + + + Number of issues in the series + + + + + Whether the comic is marked as read + + + + + Whether reading has started + + + + + Whether metadata has been edited + + + + + Whether the comic is in color + + + + + Number of pages + + + + + Comic file name + + + + + When the item was added + + + + + When the comic was last opened + + + + + Comic notes + + + + + Review text + + + + + Parent folder name + + + + + Default reading format for the folder + + + + + Whether the folder is complete + + + + + Whether the folder is marked as finished + + + + + When the folder was updated + + + SearchSingleComic @@ -2300,6 +2839,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t + + SearchSyntaxDialog + + + Common + + + + + Credits + + + + + Story + + + + + Publication + + + + + Reading & files + + + + + Folders + + + + + Search syntax + + + + + Search every comic and folder field, or build precise queries. + + + + + Quick guide + + + + + Fields (%1) + + + + + Examples + + + + + Start with a simple search + + + + + Just start typing. Plain text search across all metadata. + + + + + 1. Search everywhere + + + + + Type any text or quoted text. + + + + + 2. Target a field + + + + + Use a field name followed by : or = + + + + + 3. Combine conditions + + + + + Use AND, OR, NOT and parentheses. + + + + + Operators + + + + + : or = + + + + + contains the text + + + + + matches the complete value + + + + + greater than / at least + + + + + less than / at most + + + + + "quoted text" + + + + + keeps spaces inside one value + + + + + Dates and grouping + + + + + added in the last 7 days + + + + + added more than 30 days ago + + + + + group alternatives + + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + + + + + Find a field… + + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + + + + + Field + + + + + Description + + + + + Input + + + + + Example + + + + + Examples show the pattern—replace the values with your own. + + + + + Query + + + + + What it finds + + + + + Common filters + + + + + Unread comics + + + + + Comics in progress + + + + + Highly rated comics + + + + + Comics added in the last 7 days + + + + + Metadata + + + + + Search by series + + + + + Search by writer + + + + + Manga comics + + + + + Search textual tags + + + + + Advanced combinations + + + + + Match either writer + + + + + Group alternatives + + + + + Exclude a value + + + + + Older, highly rated comics + + + + + Copy query + + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + + + SearchVolume @@ -2406,37 +3240,59 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - set port + + Server connectivity - - Server connectivity information + + Scan to connect - - Scan it! + + Devices on this network can reach your library at the address below. - - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + + IP address - - Choose an IP address + + Port - - Port + + Set port + set port + + + + + Web interface + + + + + Copy link + + + + + Open web UI - - enable the server + + Enable the server + + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. @@ -2853,12 +3709,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + Previous versions @@ -2896,22 +3752,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save - + Cancel - + + Keyboard shortcuts + + + + + Customize the keyboard shortcuts used by the application. + + + + Edit shortcuts - + Shortcuts @@ -2919,7 +3785,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + + + + type to search diff --git a/YACReaderLibrary/yacreaderlibrary_tr.ts b/YACReaderLibrary/yacreaderlibrary_tr.ts index 7613825aa..686de1618 100644 --- a/YACReaderLibrary/yacreaderlibrary_tr.ts +++ b/YACReaderLibrary/yacreaderlibrary_tr.ts @@ -166,24 +166,24 @@ Geçerli tema JSON yüklenemedi. - + Import theme Temayı içe aktar - + JSON files (*.json);;All files (*) JSON dosyaları (*.json);;Tüm dosyalar (*) - + Could not import theme from: %1 Tema şu kaynaktan içe aktarılamadı: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed İçe aktarma başarısız oldu @@ -293,67 +293,67 @@ ComicModel - + no hayır - + yes evet - + Read Oku - + Size Boyut - + Pages Sayfalar - + Title Başlık - + File Name Dosya Adı - + Current Page Geçreli Sayfa - + Publication Date Yayın Tarihi - + Rating Reyting - + Series Seri - + Volume Hacim - + Story Arc Hikaye Arkı @@ -617,25 +617,30 @@ FileComic - + 7z not found 7z bulunamadı - + CRC error on page (%1): some of the pages will not be displayed correctly CRC hatası, sayfada (%1): bazı sayfalar düzgün görüntülenmeyecek - + Unknown error opening the file Dosya açılırken bilinmeyen hata - + Format not supported Dosya biçimi desteklenmiyor + + + Unsupported EPUB: %1 + + FolderContentView @@ -656,17 +661,22 @@ HelpAboutDialog - + Help Yardım - + System info Sistem bilgisi - + + Changelog + Değişiklik günlüğü + + + About Hakkında @@ -789,336 +799,405 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>Geçerli kitaplık, eski XML meta veri bilgileri için taranıyor.</p><p>Bu yalnızca bir kez gereklidir ve yalnızca kitaplığın YACReaderLibrary 9.8.2 veya daha eski bir sürümle oluşturulmuş olması durumunda gereklidir.</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>Geçerli kitaplıkta eksik kapaklar ve tamamlanmamış çizgi roman bilgileri denetleniyor.</p><p>Bu işlem birkaç dakika sürebilir. İşlemi durdurup daha sonra yeniden çalıştırabilirsiniz.</p> + LibraryWindow - + The selected folder doesn't contain any library. Seçilen dosya kütüphanede yok. - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? Bu kütüphane YACReaderKütüphabenin bir önceki versiyonun oluşturulmuş, güncellemeye ihtiyacın var. Şimdi güncellemek ister misin ? - + Error opening the library Haa kütüphanesini aç - Remove and delete metadata - Metadata'yı kaldır ve sil + Metadata'yı kaldır ve sil - + Old library Eski kütüphane - + Library Kütüphane - + This library was created with a newer version of YACReaderLibrary. Download the new version now? Bu kütüphane YACRKütüphanenin üst bir versiyonunda oluşturulmu. Yeni versiyonu indirmek ister misiniz ? - + Library '%1' is no longer available. Do you want to remove it? Kütüphane '%1'ulaşılabilir değil. Kaldırmak ister misin? - + Open folder... Dosyayı aç... - + Do you want remove Kaldırmak ister misin - + Error updating the library Kütüphane güncelleme sorunu - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? Kütüphane '%1 YACRKütüphanenin eski bir sürümünde oluşturulmuş, Kütüphaneyi yeniden oluşturmak ister misin? - + Set as read Okundu olarak işaretle - + Library not available Kütüphane ulaşılabilir değil - + YACReader Library YACReader Kütüphane - + Error creating the library Kütüphane oluşturma sorunu - + Update needed Güncelleme gerekli - + Library name already exists Kütüphane ismi zaten alınmış - + There is another library with the name '%1'. Bu başka bir kütüphanenin adı '%1'. - + Download new version Yeni versiyonu indir - + Delete comics Çizgi romanları sil - + All the selected comics will be deleted from your disk. Are you sure? Seçilen tüm çizgi romanlar diskten silinecek emin misin ? - - + + Set as unread Hepsini okunmadı işaretle - + Library not found Kütüphane bulunamadı - - - + + + manga manga t?r? - - - + + + comic komik - - - + + + western manga (left to right) Batı mangası (soldan sağa) - - - + + + 4koma (top to botom) 4koma (top to botom 4koma (yukarıdan aşağıya) - + library? kütüphane? - + Are you sure? Emin misin? - + Rescan library for XML info XML bilgisi için kitaplığı yeniden tarayın - - - + + + web comic web çizgi romanı - + Add new folder Yeni klasör ekle - + Delete folder Klasörü sil - + Set as uncompleted Tamamlanmamış olarak ayarla - + Set as completed Tamamlanmış olarak ayarla - + Update folder Klasörü güncelle - + Folder Klasör - + Comic Çizgi roman - + Upgrade failed Yükseltme başarısız oldu - + There were errors during library upgrade in: Kütüphane yükseltmesi sırasında hatalar oluştu: - - + + Copying comics... Çizgi romanlar kopyalanıyor... - - + + Moving comics... Çizgi romanlar taşınıyor... - + Folder name: Klasör adı: - + No folder selected Hiçbir klasör seçilmedi - + Please, select a folder first Lütfen, önce bir klasör seçiniz - + Error in path Yolda hata - + There was an error accessing the folder's path Klasörün yoluna erişilirken hata oluştu - + The selected folder and all its contents will be deleted from your disk. Are you sure? Seçilen klasör ve tüm içeriği diskinizden silinecek. Emin misin? - - + + Unable to delete Silinemedi - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. Seçili klasörleri silmeye çalışırken bir sorun oluştu. Lütfen yazma izinlerini kontrol edin ve herhangi bir uygulamanın bu klasörleri veya içerdiği dosyalardan herhangi birini kullandığından emin olun. - + Add new reading lists Yeni okuma listeleri ekle - - + + List name: Liste adı: - + Delete list/label Listeyi/Etiketi sil - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? Seçilen öğe silinecek, çizgi romanlarınız veya klasörleriniz diskinizden SİLİNMEYECEKTİR. Emin misin? - + Rename list name Listeyi yeniden adlandır - - - - + + + + Set type Türü ayarla - + + Search filters + Arama filtreleri + + + + Unread + Okunmamış + + + + In progress + Devam eden + + + + Highly rated + Yüksek puanlı + + + + Recently added + Yakın zamanda eklenen + + + + Search syntax… + Arama söz dizimi… + + + + A repair of this library is already running (%1). Wait for it to finish. + Bu kütüphanenin onarımı zaten çalışıyor (%1). Bitmesini bekleyin. + + + + The library is locked by a repair that did not finish. + Kütüphane, tamamlanmamış bir onarım tarafından kilitlendi. + + + + The library is locked by a repair started by %1. + Kütüphane, %1 tarafından başlatılan bir onarım tarafından kilitlendi. + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + Başka bir onarımın çalışmadığından eminseniz kilit kaldırılabilir. Kilit kaldırılıp devam edilsin mi? + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + + Restore recovery failed + Geri yükleme kurtarması başarısız oldu + + + Set custom cover Özel kapak ayarla - + Delete custom cover Özel kapağı sil - + Save covers Kapakları kaydet - + You are adding too many libraries. Çok fazla kütüphane ekliyorsunuz. - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1131,81 +1210,247 @@ Muhtemelen üst düzey çizgi roman klasörünüzde yalnızca bir kütüphaneye YACReaderLibrary daha fazla kütüphane oluşturmanıza engel olmaz ancak kütüphane sayısını düşük tutmalısınız. - - + + YACReader not found YACReader bulunamadı - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. YACReader bulunamadı. YACReader, YACReaderLibrary ile aynı klasöre kurulmalıdır. - + YACReader not found. There might be a problem with your YACReader installation. YACReader bulunamadı. YACReader kurulumunuzda bir sorun olabilir. - + Error Hata - + Error opening comic with third party reader. Çizgi roman üçüncü taraf okuyucuyla açılırken hata oluştu. - + + + YACReader library database (*.ydb) + YACReader kitaplık veritabanı (*.ydb) + + + + The library database backup was created at: +%1 + Kitaplık veritabanı yedeği şu konumda oluşturuldu: +%1 + + + + Unable to create the library database backup: +%1 + Kitaplık veritabanı yedeği oluşturulamadı: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + Geri yüklemeden önce YACReaderLibraryServer'ı ve bu kitaplığı kullanan diğer tüm YACReader uygulamalarını kapatın. Devam edilsin mi? + + + + Restoring library database... + Kitaplık veritabanı geri yükleniyor... + + + + The current library database is invalid. Restore the selected backup anyway? + Geçerli kitaplık veritabanı geçersiz. Seçilen yedek yine de geri yüklensin mi? + + + + + The library maintenance lock may be stale. Remove it and retry? + Kitaplık bakım kilidi eski kalmış olabilir. Kaldırıp yeniden denensin mi? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +Kurtarmayı yeniden denemeden önce YACReaderLibrary'yi yeniden başlatın. + + + + The library database was restored successfully. Update the library now? + Kitaplık veritabanı başarıyla geri yüklendi. Kitaplık şimdi güncellensin mi? + + + + Library database damaged + Kitaplık veritabanı hasarlı + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + '%1' kitaplığının veritabanı hasarlı olduğundan normal güncellemeler, bakım ve yedeklemeler kullanılamıyor. YACReader veritabanını onarmayı deneyebilir. Bazı hasarlı veriler kurtarılamayabilir. Mevcut yedekler değiştirilmeyecektir. + + + + Attempt repair + Onarmayı dene + + + + Restore a backup... + Bir yedeği geri yükle... + + + + Repairing library database... + Kitaplık veritabanı onarılıyor... + + + + + + Library database repair + Kitaplık veritabanını onar + + + + Another maintenance operation is currently using this library. Try again after it finishes. + Başka bir bakım işlemi şu anda bu kitaplığı kullanıyor. İşlem bittikten sonra yeniden deneyin. + + + + The library database is already valid. + Kitaplık veritabanı zaten geçerli. + + + + Library database repaired + Kitaplık veritabanı onarıldı + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + Kitaplık veritabanı dizinleri yeniden oluşturularak onarıldı. Hasarlı özgün dosya şu konumda korundu: +%1 + + + + Library database rebuilt + Kitaplık veritabanı yeniden oluşturuldu + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + Kitaplık veritabanı başarıyla yeniden oluşturuldu. Hasarlı özgün dosya şu konumda korundu: +%1 + +Kitaplık şimdi güncellensin mi? + + + + + +The damaged original was preserved at: +%1 + + +Hasarlı özgün dosya şu konumda korundu: +%1 + + + + Library database repair failed + Kitaplık veritabanı onarılamadı + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + Kitaplık veritabanı onarılamadı: +%1%2 + +Kitaplık menüsünden bir yedeği geri yükleyebilir veya kitaplığı yeniden oluşturabilirsiniz. + + + + Remove and delete metadata and backups + Meta verileri ve yedekleri kaldır ve sil + + + Library info Kütüphane bilgisi - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. Seçilen çizgi romanlar silinmeye çalışılırken bir sorun oluştu. Lütfen seçilen dosyalarda veya klasörleri içeren yazma izinlerini kontrol edin. - + Assign comics numbers Çizgi roman numaraları ata - + Assign numbers starting in: Şunlardan başlayarak numaralar ata: - + Invalid image Geçersiz resim - + The selected file is not a valid image. Seçilen dosya geçerli bir resim değil. - + Error saving cover Kapak kaydedilirken hata oluştu - + There was an error saving the cover image. Kapak resmi kaydedilirken bir hata oluştu. - + Remove comics Çizgi romanları kaldır - + Comics will only be deleted from the current label/list. Are you sure? Çizgi romanlar yalnızca mevcut etiketten/listeden silinecektir. Emin misin? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + Onarılan: %1 +Başarısız: %2 +Eksik dosyalar: %3 + LibraryWindowActions @@ -1263,384 +1508,425 @@ YACReaderLibrary daha fazla kütüphane oluşturmanıza engel olmaz ancak kütü + Back up library database + Kitaplık veritabanını yedekle + + + + Create a backup of the current library database + Geçerli kitaplık veritabanının yedeğini oluştur + + + + Restore library database backup + Kitaplık veritabanı yedeğini geri yükle + + + + Restore the current library database from a backup + Geçerli kitaplık veritabanını bir yedekten geri yükle + + + + + Repair covers and comic info + Kapakları ve çizgi roman bilgilerini onar + + + + Retry comics with missing covers or incomplete information + Kapağı eksik veya bilgileri tamamlanmamış çizgi romanları yeniden işle + + + Rename library Kütüphaneyi yeniden adlandır - + Rename current library Kütüphaneyi adlandır - + Remove library Kütüphaneyi sil - + Remove current library from your collection Kütüphaneyi koleksiyonundan kaldır - + Rescan library for XML info XML bilgisi için kitaplığı yeniden tarayın - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. Komik dosyalara gömülü XML bilgilerini bulmaya çalışır. Bunu yalnızca kitaplık 9.8.2 veya önceki sürümlerle oluşturulmuşsa veya XML bilgilerini dosyalara eklemek için üçüncü taraf yazılım kullanıyorsanız yapmanız gerekir. - + + Open library folder... + Kütüphane klasörünü aç... + + + + Open the root folder of the current library + Geçerli kütüphanenin kök klasörünü aç + + + Show library info Kitaplık bilgilerini göster - + Show information about the current library Geçerli kitaplık hakkındaki bilgileri göster - + Open current comic Seçili çizgi romanı aç - + Open current comic on YACReader YACReader'ı geçerli çizgi roman okuyucsu seç - + Save selected covers to... Seçilen kapakları şuraya kaydet... - + Save covers of the selected comics as JPG files Seçilen çizgi romanların kapaklarını JPG dosyaları olarak kaydet - - + + Set as read Okundu olarak işaretle - + Set comic as read Çizgi romanı okundu olarak işaretle - - + + Set as unread Hepsini okunmadı işaretle - + Set comic as unread Çizgi Romanı okunmadı olarak seç - - + + manga manga t?r? - + Set issue as manga Sayıyı manga olarak ayarla - - + + comic komik - + Set issue as normal Sayıyı normal olarak ayarla - + western manga batı mangası - + Set issue as western manga Konuyu western mangası olarak ayarla - - + + web comic web çizgi romanı - + Set issue as web comic Sorunu web çizgi romanı olarak ayarla - - + + yonkoma d?rt panelli - + Set issue as yonkoma Sorunu yonkoma olarak ayarla - + Show/Hide marks Altçizgileri aç/kapa - + Show or hide read marks Okundu işaretlerini göster yada gizle - + Show/Hide recent indicator Son göstergeyi Göster/Gizle - + Show or hide recent indicator Son göstergeyi göster veya gizle - - + + Fullscreen mode on/off Tam ekran modu açık/kapalı - + Help, About YACReader Yardım, Bigli, YACReader - + Add new folder Yeni klasör ekle - + Add new folder to the current library Geçerli kitaplığa yeni klasör ekle - + Delete folder Klasörü sil - + Delete current folder from disk Geçerli klasörü diskten sil - + Select root node Kökü seçin - + Expand all nodes Tüm düğümleri büyüt - + Collapse all nodes Tüm düğümleri kapat - + Show options dialog Ayarları göster - + Show comics server options dialog Çizgi romanların server ayarlarını göster - - + + Change between comics views Çizgi roman görünümleri arasında değiştir - + Open folder... Dosyayı aç... - + Set as uncompleted Tamamlanmamış olarak ayarla - + Set as completed Tamamlanmış olarak ayarla - + Set custom cover Özel kapak ayarla - + Delete custom cover Özel kapağı sil - + western manga (left to right) Batı mangası (soldan sağa) - + Open containing folder... Klasör açılıyor... - + Reset comic rating Çizgi roman reytingini sıfırla - + Select all comics Tüm çizgi romanları seç - + Edit Düzenle - + Assign current order to comics Geçerli sırayı çizgi romanlara ata - + Update cover Kapağı güncelle - + Delete selected comics Seçili çizgi romanları sil - + Delete metadata from selected comics Seçilen çizgi romanlardan meta verileri sil - + Download tags from Comic Vine Etiketleri Comic Vine sitesinden indir - + Focus search line Arama satırına odaklan - + Focus comics view Çizgi roman görünümüne odaklanın - + Edit shortcuts Kısayolları düzenle - + &Quit &Çıkış - + Update folder Klasörü güncelle - + Update current folder Geçerli klasörü güncelle - + Scan legacy XML metadata Eski XML meta verilerini tarayın - + Add new reading list Yeni okuma listesi ekle - + Add a new reading list to the current library Geçerli kitaplığa yeni bir okuma listesi ekle - + Remove reading list Okuma listesini kaldır - + Remove current reading list from the library Geçerli okuma listesini kütüphaneden kaldır - + Add new label Yeni etiket ekle - + Add a new label to this library Bu kitaplığa yeni bir etiket ekle - + Rename selected list Seçilen listeyi yeniden adlandır - + Rename any selected labels or lists Seçilen etiketleri ya da listeleri yeniden adlandır - + Add to... Şuraya ekle... - + Favorites Favoriler - + Add selected comics to favorites list Seçilen çizgi romanları favoriler listesine ekle @@ -1692,78 +1978,78 @@ YACReaderLibrary daha fazla kütüphane oluşturmanıza engel olmaz ancak kütü Dış görünüş - + Options Ayarlar - + Language Dil - + Application language Uygulama dili - + System default Sistem varsayılanı - + Tray icon settings (experimental) Tepsi simgesi ayarları (deneysel) - + Close to tray Tepsiyi kapat - + Start into the system tray Sistem tepsisinde başlat - + Edit Comic Vine API key Comic Vine API anahtarını düzenle - + Comic Vine API key Comic Vine API anahtarı - + ComicInfo.xml legacy support ComicInfo.xml eski desteği - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics Yeni çizgi roman eklerken meta verileri ComicInfo.xml'den içe aktarın - + Consider 'recent' items added or updated since X days ago X gün öncesinden bu yana eklenen veya güncellenen 'en son' öğeleri göz önünde bulundurun - + Third party reader Üçüncü taraf okuyucu - + Write {comic_file_path} where the path should go in the command Komutta yolun gitmesi gereken yere {comic_file_path} yazın - + Clear Temizle @@ -1923,7 +2209,7 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y Genel - + Restart is needed Yeniden başlatılmalı @@ -2293,6 +2579,279 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y Sayı %1, bulunan : %2 + + SearchFieldRegistry + + + Text, quoted text + Metin, tırnak içinde metin + + + + Integer + Tam sayı + + + + Boolean (true / false) + Mantıksal değer (true / false) + + + + Integer (number of days) + Tam sayı (gün sayısı) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + Numaralandırma (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + Çizgi roman başlığı + + + + Series name + Seri adı + + + + Issue number + Sayı numarası + + + + Volume identifier + Cilt tanımlayıcısı + + + + Reading format + Okuma biçimi + + + + Comic rating + Çizgi roman puanı + + + + Textual tags + Metin etiketleri + + + + Writer credit + Yazar bilgisi + + + + Penciller credit + Çizer bilgisi + + + + Inker credit + Mürekkepleme sanatçısı bilgisi + + + + Colorist credit + Renklendirme sanatçısı bilgisi + + + + Letterer credit + Harfleme sanatçısı bilgisi + + + + Cover artist credit + Kapak sanatçısı bilgisi + + + + Editor credit + Editör bilgisi + + + + Story arc name + Hikâye yayı adı + + + + Position within a story arc + Hikâye yayı içindeki konum + + + + Number of issues in a story arc + Hikâye yayındaki sayı adedi + + + + Characters appearing in the comic + Çizgi romanda görünen karakterler + + + + Teams appearing in the comic + Çizgi romanda görünen ekipler + + + + Locations appearing in the comic + Çizgi romanda görünen yerler + + + + Primary character or team + Ana karakter veya ekip + + + + Comic synopsis + Çizgi roman özeti + + + + Publisher name + Yayınevi adı + + + + Publishing imprint + Yayın markası + + + + Publication format + Yayın biçimi + + + + Recommended age rating + Önerilen yaş derecelendirmesi + + + + Comic genre + Çizgi roman türü + + + + ISO language code + ISO dil kodu + + + + Publication date metadata + Yayın tarihi üst verisi + + + + Series grouping metadata + Seri gruplandırma üst verisi + + + + Alternate series name + Alternatif seri adı + + + + Alternate issue number + Alternatif sayı numarası + + + + Alternate series issue count + Alternatif serideki sayı adedi + + + + Number of issues in the series + Serideki sayı adedi + + + + Whether the comic is marked as read + Çizgi romanın okundu olarak işaretlenip işaretlenmediği + + + + Whether reading has started + Okumaya başlanıp başlanmadığı + + + + Whether metadata has been edited + Üst verinin düzenlenip düzenlenmediği + + + + Whether the comic is in color + Çizgi romanın renkli olup olmadığı + + + + Number of pages + Sayfa sayısı + + + + Comic file name + Çizgi roman dosya adı + + + + When the item was added + Öğenin ne zaman eklendiği + + + + When the comic was last opened + Çizgi romanın en son ne zaman açıldığı + + + + Comic notes + Çizgi roman notları + + + + Review text + İnceleme metni + + + + Parent folder name + Üst klasör adı + + + + Default reading format for the folder + Klasörün varsayılan okuma biçimi + + + + Whether the folder is complete + Klasörün tamamlanmış olup olmadığı + + + + Whether the folder is marked as finished + Klasörün bitmiş olarak işaretlenip işaretlenmediği + + + + When the folder was updated + Klasörün ne zaman güncellendiği + + SearchSingleComic @@ -2312,6 +2871,301 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y Tam eşleme aramasını kullanın. Addaki bazı sözcüklerle eşleşen ciltleri bulmak istiyorsanız devre dışı bırakın. + + SearchSyntaxDialog + + + Common + Genel + + + + Credits + Katkıda bulunanlar + + + + Story + Hikâye + + + + Publication + Yayın + + + + Reading & files + Okuma ve dosyalar + + + + Folders + Klasörler + + + + Search syntax + Arama söz dizimi + + + + Search every comic and folder field, or build precise queries. + Tüm çizgi roman ve klasör alanlarında arama yapın veya hassas sorgular oluşturun. + + + + Quick guide + Hızlı kılavuz + + + + Fields (%1) + Alanlar (%1) + + + + Examples + Örnekler + + + + Start with a simple search + Basit bir aramayla başlayın + + + + Just start typing. Plain text search across all metadata. + Yazmaya başlamanız yeterlidir. Düz metin tüm üst verilerde aranır. + + + + 1. Search everywhere + 1. Her yerde ara + + + + Type any text or quoted text. + Herhangi bir metin veya tırnak içinde metin yazın. + + + + 2. Target a field + 2. Bir alanı hedefle + + + + Use a field name followed by : or = + Bir alan adının ardından : veya = kullanın + + + + 3. Combine conditions + 3. Koşulları birleştir + + + + Use AND, OR, NOT and parentheses. + AND, OR, NOT ve parantez kullanın. + + + + Operators + İşleçler + + + + : or = + : veya = + + + + contains the text + metni içerir + + + + matches the complete value + değerin tamamıyla eşleşir + + + + greater than / at least + büyüktür / en az + + + + less than / at most + küçüktür / en fazla + + + + "quoted text" + "tırnak içinde metin" + + + + keeps spaces inside one value + boşlukları tek bir değer içinde tutar + + + + Dates and grouping + Tarihler ve gruplandırma + + + + added in the last 7 days + son 7 gün içinde eklendi + + + + added more than 30 days ago + 30 günden daha uzun süre önce eklendi + + + + group alternatives + alternatifleri gruplandırır + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + İpuçları: Boşluklar AND gibi davranır ve aramalar büyük/küçük harfe duyarlı değildir. Bir değerde boşluk kullanmak için tırnak işaretlerini kullanın. + + + + Find a field… + Alan bul… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + Metin doğrudan veya tırnak içinde girilebilir. Tam sayı alanları <, <=, > ve >= işleçlerini destekler. Tarih alanlarında tam sayı gün sayısını belirtir (added>7, son 7 gün içinde eklenenler anlamına gelir). + + + + Field + Alan + + + + Description + Açıklama + + + + Input + Girdi + + + + Example + Örnek + + + + Examples show the pattern—replace the values with your own. + Örnekler kalıbı gösterir; değerleri kendi değerlerinizle değiştirin. + + + + Query + Sorgu + + + + What it finds + Bulduğu öğeler + + + + Common filters + Yaygın filtreler + + + + Unread comics + Okunmamış çizgi romanlar + + + + Comics in progress + Okunmakta olan çizgi romanlar + + + + Highly rated comics + Yüksek puanlı çizgi romanlar + + + + Comics added in the last 7 days + Son 7 gün içinde eklenen çizgi romanlar + + + + Metadata + Üst veri + + + + Search by series + Seriye göre ara + + + + Search by writer + Yazara göre ara + + + + Manga comics + Manga çizgi romanları + + + + Search textual tags + Metin etiketlerinde ara + + + + Advanced combinations + Gelişmiş birleşimler + + + + Match either writer + Yazarlardan herhangi biriyle eşleştir + + + + Group alternatives + Alternatifleri gruplandır + + + + Exclude a value + Bir değeri hariç tut + + + + Older, highly rated comics + Eski, yüksek puanlı çizgi romanlar + + + + Copy query + Sorguyu kopyala + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + Boşluklar AND gibi davranır. İfadeler için tırnak işaretlerini, gruplandırmayı denetlemek için parantezleri kullanın. + + SearchVolume @@ -2417,39 +3271,81 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y ServerConfigDialog - + + + Server connectivity + Sunucu bağlantısı + + + + Scan to connect + Bağlanmak için tarayın + + + + Devices on this network can reach your library at the address below. + Bu ağdaki cihazlar aşağıdaki adresten kitaplığınıza erişebilir. + + + + IP address + IP adresi + + + Port Liman - + + Web interface + Web arayüzü + + + + Copy link + Bağlantıyı kopyala + + + + Open web UI + Web arayüzünü aç + + + + Enable the server + Sunucuyu etkinleştir + + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader, iOS ve Android için kullanılabilir. <a href='https://ios.yacreader.com'>iOS</a> veya <a href='https://android.yacreader.com'>Android</a> sürümünü keşfedin. + + enable the server - erişilebilir server + erişilebilir server - - set port - Port Ayarla + + Set port + set port + Portu ayarla - Server connectivity information - Sunucu bağlantı bilgileri + Sunucu bağlantı bilgileri - Scan it! - Tara! + Tara! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader, iOS ve Android cihazlarda kullanılabilir.<br/>Bunu <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> veya <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a> için keşfedin. + YACReader, iOS ve Android cihazlarda kullanılabilir.<br/>Bunu <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> veya <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a> için keşfedin. - Choose an IP address - IP adresi seçin + IP adresi seçin @@ -2868,14 +3764,14 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y YACReader::WhatsNewDialog - + Release notes are not available. - + Sürüm notları kullanılamıyor. - + Previous versions - + Önceki sürümler @@ -2911,22 +3807,32 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y YACReaderOptionsDialog - + Save Kaydet - + Cancel Vazgeç - + + Keyboard shortcuts + Klavye kısayolları + + + + Customize the keyboard shortcuts used by the application. + Uygulama tarafından kullanılan klavye kısayollarını özelleştirin. + + + Edit shortcuts Kısayolları düzenle - + Shortcuts Kısayollar @@ -2934,7 +3840,12 @@ Otomatik güncellemeyi durdurmak için Kitaplıklar başlığının yanındaki y YACReaderSearchLineEdit - + + Search filters + Arama filtreleri + + + type to search aramak için yazınız diff --git a/YACReaderLibrary/yacreaderlibrary_zh_CN.ts b/YACReaderLibrary/yacreaderlibrary_zh_CN.ts index 579622e1a..b2ae00441 100644 --- a/YACReaderLibrary/yacreaderlibrary_zh_CN.ts +++ b/YACReaderLibrary/yacreaderlibrary_zh_CN.ts @@ -98,17 +98,17 @@ Light - 亮度 + 明亮 Dark - 黑暗的 + 暗黑 Custom - 风俗 + 自定义 @@ -123,12 +123,12 @@ Light: - 光: + 明亮: Dark: - 黑暗的: + 暗黑: @@ -166,24 +166,24 @@ 无法加载当前主题 JSON。 - + Import theme 导入主题 - + JSON files (*.json);;All files (*) JSON 文件 (*.json);;所有文件 (*) - + Could not import theme from: %1 无法从以下位置导入主题: %1 - + Could not import theme from: %1 @@ -194,7 +194,7 @@ %2 - + Import failed 导入失败 @@ -204,7 +204,7 @@ Hide comic flow - 隐藏 Comic Flow + 隐藏漫画页面流 @@ -222,7 +222,7 @@ imprint - 出版品牌 + 压印 @@ -293,67 +293,67 @@ ComicModel - + no - + yes - + Read 阅读 - + Size 大小 - + Pages 页数 - + Title 标题 - + Current Page 当前页 - + File Name 文件名 - + Rating 评分 - + Series 系列 - + Volume - + Story Arc 故事线 - + Publication Date 出版日期 @@ -617,22 +617,27 @@ FileComic - + Format not supported 不支持的文件格式 - + 7z not found 未找到 7z - + Unknown error opening the file 打开文件时出现未知错误 - + + Unsupported EPUB: %1 + 不支持的 EPUB 格式:%1 + + + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 页 CRC 校验失败: 部分页面将无法正确显示 @@ -656,20 +661,25 @@ HelpAboutDialog - + Help 帮助 - + About 关于 - + System info 系统信息 + + + Changelog + 更新日志 + ImportComicsInfoDialog @@ -789,233 +799,237 @@ <p>The current library is being updated. For faster updates, please, update your libraries frequently.</p><p>You can stop the process and continue updating this library later.</p> <p>正在更新当前库。要获得更快的更新,请经常更新您的库。</p><p>您可以停止该进程,稍后继续更新操作。</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>正在检查当前漫画库中缺失的封面和不完整的漫画信息。</p><p>这可能需要几分钟。您可以停止该过程,稍后再重新运行。</p> + LibraryWindow - + The selected folder doesn't contain any library. 所选文件夹不包含任何库。 - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? 此库是使用旧版本的YACReaderLibrary创建的. 它需要更新. 现在更新? - + Upgrade failed 更新失败 - + Comic 漫画 - - - + + + comic 漫画 - - - + + + manga 日本漫画 - + Folder name: 文件夹名称: - + The selected folder and all its contents will be deleted from your disk. Are you sure? 所选文件夹及其所有内容将从磁盘中删除。 你确定吗? - + Rescan library for XML info 重新扫描库的 XML 信息 - + Error opening the library 打开库时出错 - - + + YACReader not found YACReader 未找到 - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. 尝试删除所选文件夹时出现问题。 请检查写入权限,并确保没有其他应用程序在使用这些文件夹或文件。 - + Rename list name 重命名列表 - Remove and delete metadata - 移除并删除元数据 + 移除并删除元数据 - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. 未找到YACReader. YACReader应安装在与YACReaderLibrary相同的文件夹中. - + Old library 旧的库 - + Set as completed 设为已完成 - + There was an error accessing the folder's path 访问文件夹的路径时出错 - + Library - + Comics will only be deleted from the current label/list. Are you sure? 漫画只会从当前标签/列表中删除。 你确定吗? - + This library was created with a newer version of YACReaderLibrary. Download the new version now? 此库是使用较新版本的YACReaderLibrary创建的。 立即下载新版本? - - + + Moving comics... 移动漫画中... - - + + Copying comics... 复制漫画中... - + Library '%1' is no longer available. Do you want to remove it? 库 '%1' 不再可用。 你想删除它吗? - - - + + + web comic 网络漫画 - + Open folder... 打开文件夹... - + Set custom cover 设置自定义封面 - + Delete custom cover 删除自定义封面 - + Error 错误 - + Error opening comic with third party reader. 使用第三方阅读器打开漫画时出错。 - + Do you want remove 你想要删除 - + Set as uncompleted 设为未完成 - + Error in path 路径错误 - + Error updating the library 更新库时出错 - + Folder 文件夹 - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? 所选项目将被删除,您的漫画或文件夹将不会从您的磁盘中删除。 你确定吗? - - - + + + western manga (left to right) 欧美漫画(从左到右) - - + + List name: 列表名称: - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? 库 '%1' 是通过旧版本的YACReaderLibrary创建的。 必须再次创建。 你想现在创建吗? - + Save covers 保存封面 - + YACReader not found. There might be a problem with your YACReader installation. 未找到YACReader. YACReader的安装可能有问题. - + Add new reading lists 添加新的阅读列表 - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1028,182 +1042,413 @@ YACReaderLibrary will not stop you from creating more libraries but you should k YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低的库数量来提升性能。 - + Set as read 设为已读 - + Assign comics numbers 分配漫画编号 - + There were errors during library upgrade in: 漫画库更新时出现错误: - + Please, select a folder first 请先选择一个文件夹 - + Library not available 库不可用 - + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. 尝试删除所选漫画时出现问题。 请检查所选文件或包含文件夹中的写入权限。 - + YACReader Library YACReader 库 - + Error creating the library 创建库时出错 - + You are adding too many libraries. 您添加的库太多了。 - + Update folder 更新文件夹 - + Update needed 需要更新 - + Library name already exists 库名已存在 - + There is another library with the name '%1'. 已存在另一个名为'%1'的库。 - + Delete folder 删除文件夹 - + Assign numbers starting in: 从以下位置开始分配编号: - + Download new version 下载新版本 - - - - + + Search filters + 搜索筛选条件 + + + + Unread + 未读 + + + + In progress + 阅读中 + + + + Highly rated + 高评分 + + + + Recently added + 最近添加 + + + + Search syntax… + 搜索语法… + + + + + + Set type 设置类型 - + + A repair of this library is already running (%1). Wait for it to finish. + 此库的修复已在运行中(%1)。请等待其完成。 + + + + The library is locked by a repair that did not finish. + 库已被一个未完成的修复锁定。 + + + + The library is locked by a repair started by %1. + 库已被 %1 启动的修复锁定。 + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + 如果您确定没有其他修复正在运行,可以移除该锁定。移除锁定并继续? + + + + Package operation failed + 打包操作失败 + + + + The covers package operation could not be completed. + 封面包操作无法完成。 + + + + Restore recovery failed + 恢复操作修复失败 + + + + + YACReader library database (*.ydb) + YACReader 资料库数据库 (*.ydb) + + + + The library database backup was created at: +%1 + 资料库数据库备份已创建于: +%1 + + + + Unable to create the library database backup: +%1 + 无法创建资料库数据库备份: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + 恢复前请关闭 YACReaderLibraryServer 以及正在使用此资料库的所有其他 YACReader 应用程序。是否继续? + + + + Restoring library database... + 正在恢复资料库数据库... + + + + The current library database is invalid. Restore the selected backup anyway? + 当前资料库数据库无效。仍要恢复所选备份吗? + + + + + The library maintenance lock may be stale. Remove it and retry? + 资料库维护锁可能已失效。是否移除并重试? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +再次尝试恢复前,请重新启动 YACReaderLibrary。 + + + + The library database was restored successfully. Update the library now? + 资料库数据库已成功恢复。是否立即更新资料库? + + + + Library database damaged + 资料库数据库已损坏 + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + 资料库“%1”的数据库已损坏,因此无法执行常规更新、维护和备份。YACReader 可以尝试修复数据库。部分损坏的数据可能无法恢复。现有备份不会被更改。 + + + + Attempt repair + 尝试修复 + + + + Restore a backup... + 恢复备份... + + + + Repairing library database... + 正在修复资料库数据库... + + + + + + Library database repair + 修复资料库数据库 + + + + Another maintenance operation is currently using this library. Try again after it finishes. + 另一个维护操作正在使用此资料库。请在其完成后重试。 + + + + The library database is already valid. + 资料库数据库已经有效。 + + + + Library database repaired + 资料库数据库已修复 + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + 已通过重建索引修复资料库数据库。损坏的原始文件已保存在: +%1 + + + + Library database rebuilt + 资料库数据库已重建 + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + 资料库数据库已成功重建。损坏的原始文件已保存在: +%1 + +是否立即更新资料库? + + + + + +The damaged original was preserved at: +%1 + + +损坏的原始文件已保存在: +%1 + + + + Library database repair failed + 资料库数据库修复失败 + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + 无法修复资料库数据库: +%1%2 + +您可以从“资料库”菜单恢复备份,或重新创建资料库。 + + + + Remove and delete metadata and backups + 移除并删除元数据和备份 + + + Library info 图书馆信息 - + Invalid image 图片无效 - + The selected file is not a valid image. 所选文件不是有效图像。 - + Error saving cover 保存封面时出错 - + There was an error saving the cover image. 保存封面图像时出错。 - + Delete comics 删除漫画 - + Add new folder 添加新的文件夹 - + Delete list/label 删除 列表/标签 - + No folder selected 没有选中的文件夹 - + All the selected comics will be deleted from your disk. Are you sure? 所有选定的漫画都将从您的磁盘中删除。你确定吗? - + Remove comics 移除漫画 - - + + Set as unread 设为未读 - + Library not found 未找到库 - - + + Unable to delete 无法删除 - - - + + + 4koma (top to botom) 四格漫画(从上到下) - + library? 库? - + Are you sure? 你确定吗? + + + Repaired: %1 +Failed: %2 +Missing files: %3 + 已修复:%1 +失败:%2 +文件缺失:%3 + LibraryWindowActions @@ -1261,384 +1506,425 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 + Back up library database + 备份资料库数据库 + + + + Create a backup of the current library database + 创建当前资料库数据库的备份 + + + + Restore library database backup + 恢复资料库数据库备份 + + + + Restore the current library database from a backup + 从备份恢复当前资料库数据库 + + + + + Repair covers and comic info + 修复封面和漫画信息 + + + + Retry comics with missing covers or incomplete information + 重新处理缺少封面或信息不完整的漫画 + + + Rename library 重命名库 - + Rename current library 重命名当前库 - + Remove library 移除库 - + Remove current library from your collection 从您的集合中移除当前库 - + Rescan library for XML info 重新扫描库的 XML 信息 - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. 尝试查找漫画文件内嵌的 XML 信息。只有当创建库的 YACReaderLibrary 版本低于 9.8.2 或者使用第三方软件嵌入 XML 信息时,才需要执行该操作。 - + + Open library folder... + 打开库文件夹... + + + + Open the root folder of the current library + 打开当前库的根文件夹 + + + Show library info 显示图书馆信息 - + Show information about the current library 显示当前库的信息 - + Open current comic 打开当前漫画 - + Open current comic on YACReader 用YACReader打开漫画 - + Save selected covers to... 选中的封面保存到... - + Save covers of the selected comics as JPG files 保存所选的封面为jpg - - + + Set as read 设为已读 - + Set comic as read 漫画设为已读 - - + + Set as unread 设为未读 - + Set comic as unread 漫画设为未读 - - + + manga 日本漫画 - + Set issue as manga - 将问题设置为漫画 + 设置为漫画 - - + + comic 漫画 - + Set issue as normal 设置漫画为 - + western manga 欧美漫画 - + Set issue as western manga 设置为欧美漫画 - - + + web comic 网络漫画 - + Set issue as web comic 设置为网络漫画 - - + + yonkoma 四格漫画 - + Set issue as yonkoma 设置为四格漫画 - + Show/Hide marks 显示/隐藏标记 - + Show or hide read marks 显示或隐藏阅读标记 - + Show/Hide recent indicator 显示/隐藏最近的指示标志 - + Show or hide recent indicator 显示或隐藏最近的指示标志 - - + + Fullscreen mode on/off 全屏模式 开/关 - + Help, About YACReader 帮助, 关于 YACReader - + Add new folder 添加新的文件夹 - + Add new folder to the current library 在当前库下添加新的文件夹 - + Delete folder 删除文件夹 - + Delete current folder from disk 从磁盘上删除当前文件夹 - + Select root node 选择根节点 - + Expand all nodes 展开所有节点 - + Collapse all nodes 折叠所有节点 - + Show options dialog 显示选项对话框 - + Show comics server options dialog 显示漫画服务器选项对话框 - - + + Change between comics views 漫画视图之间的变化 - + Open folder... 打开文件夹... - + Set as uncompleted 设为未完成 - + Set as completed 设为已完成 - + Set custom cover 设置自定义封面 - + Delete custom cover 删除自定义封面 - + western manga (left to right) 欧美漫画(从左到右) - + Open containing folder... 打开包含文件夹... - + Reset comic rating 重置漫画评分 - + Select all comics 全选漫画 - + Edit 编辑 - + Assign current order to comics 将当前序号分配给漫画 - + Update cover 更新封面 - + Delete selected comics 删除所选的漫画 - + Delete metadata from selected comics 从选定的漫画中删除元数据 - + Download tags from Comic Vine 从 Comic Vine 下载标签 - + Focus search line 聚焦于搜索行 - + Focus comics view 聚焦于漫画视图 - + Edit shortcuts 编辑快捷键 - + &Quit 退出(&Q) - + Update folder 更新文件夹 - + Update current folder 更新当前文件夹 - + Scan legacy XML metadata 扫描旧版 XML 元数据 - + Add new reading list 添加新的阅读列表 - + Add a new reading list to the current library 在当前库添加新的阅读列表 - + Remove reading list 移除阅读列表 - + Remove current reading list from the library 从当前库移除阅读列表 - + Add new label 添加新标签 - + Add a new label to this library 在当前库添加标签 - + Rename selected list 重命名列表 - + Rename any selected labels or lists 重命名任何选定的标签或列表 - + Add to... 添加到... - + Favorites 收藏夹 - + Add selected comics to favorites list 将所选漫画添加到收藏夹列表 @@ -1705,7 +1991,7 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 恢复默认值 - + Close to tray 关闭至托盘 @@ -1725,7 +2011,7 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 1小时 - + Start into the system tray 启动至系统托盘 @@ -1747,35 +2033,35 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 Appearance - 外貌 + 外观 - + Language 语言 - + Application language 应用程序语言 - + System default 系统默认 - + Third party reader 第三方阅读器 - + Write {comic_file_path} where the path should go in the command 在命令中应将路径写入 {comic_file_path} - + Clear 清空 @@ -1805,7 +2091,7 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 更新库时比较文件的修改日期(不推荐) - + Import metadata from ComicInfo.xml when adding new comics 添加新漫画时从 ComicInfo.xml 导入元数据 @@ -1820,22 +2106,22 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 4小时 - + Options 选项 - + Comic Vine API key Comic Vine API 密匙 - + Edit Comic Vine API key 编辑Comic Vine API 密匙 - + Tray icon settings (experimental) 托盘图标设置 (实验特性) @@ -1861,7 +2147,7 @@ YACReaderLibrary不会阻止您创建更多的库,但是您应该保持较低 间隔: - + ComicInfo.xml legacy support ComicInfo.xml 旧版支持 @@ -1892,7 +2178,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 常规 - + Consider 'recent' items added or updated since X days ago 参考自 X 天前添加或更新的“最近”项目 @@ -1909,7 +2195,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Comic Flow - Comic Flow + 漫画页面流 @@ -1917,7 +2203,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 网格视图 - + Restart is needed 需要重启 @@ -2049,7 +2335,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Imprint: - 印记: + 压印: @@ -2059,7 +2345,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Issue number: - 发行刊号: + 期刊号: @@ -2287,6 +2573,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 搜索结果: %1 + + SearchFieldRegistry + + + Text, quoted text + 文本、带引号的文本 + + + + Integer + 整数 + + + + Boolean (true / false) + 布尔值 (true / false) + + + + Integer (number of days) + 整数(天数) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + 枚举 (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + 漫画标题 + + + + Series name + 系列名称 + + + + Issue number + 期号 + + + + Volume identifier + 卷标识 + + + + Reading format + 阅读格式 + + + + Comic rating + 漫画评分 + + + + Textual tags + 文本标签 + + + + Writer credit + 编剧署名 + + + + Penciller credit + 线稿师署名 + + + + Inker credit + 墨线师署名 + + + + Colorist credit + 上色师署名 + + + + Letterer credit + 嵌字师署名 + + + + Cover artist credit + 封面画师署名 + + + + Editor credit + 编辑署名 + + + + Story arc name + 故事线名称 + + + + Position within a story arc + 故事线中的位置 + + + + Number of issues in a story arc + 故事线中的期数 + + + + Characters appearing in the comic + 漫画中出现的角色 + + + + Teams appearing in the comic + 漫画中出现的团队 + + + + Locations appearing in the comic + 漫画中出现的地点 + + + + Primary character or team + 主要角色或团队 + + + + Comic synopsis + 漫画简介 + + + + Publisher name + 出版社名称 + + + + Publishing imprint + 出版品牌 + + + + Publication format + 出版格式 + + + + Recommended age rating + 建议年龄分级 + + + + Comic genre + 漫画类型 + + + + ISO language code + ISO 语言代码 + + + + Publication date metadata + 出版日期元数据 + + + + Series grouping metadata + 系列分组元数据 + + + + Alternate series name + 备用系列名称 + + + + Alternate issue number + 备用期号 + + + + Alternate series issue count + 备用系列期数 + + + + Number of issues in the series + 系列期数 + + + + Whether the comic is marked as read + 漫画是否标记为已读 + + + + Whether reading has started + 是否已开始阅读 + + + + Whether metadata has been edited + 元数据是否已编辑 + + + + Whether the comic is in color + 漫画是否为彩色 + + + + Number of pages + 页数 + + + + Comic file name + 漫画文件名 + + + + When the item was added + 项目的添加时间 + + + + When the comic was last opened + 漫画上次打开时间 + + + + Comic notes + 漫画备注 + + + + Review text + 评论文本 + + + + Parent folder name + 上级文件夹名称 + + + + Default reading format for the folder + 文件夹的默认阅读格式 + + + + Whether the folder is complete + 文件夹是否完整 + + + + Whether the folder is marked as finished + 文件夹是否标记为已完结 + + + + When the folder was updated + 文件夹的更新时间 + + SearchSingleComic @@ -2306,6 +2865,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 使用精确匹配搜索。如果您想要查找与名称中某些单词匹配的卷,请禁用。 + + SearchSyntaxDialog + + + Common + 常用 + + + + Credits + 创作人员 + + + + Story + 故事 + + + + Publication + 出版 + + + + Reading & files + 阅读和文件 + + + + Folders + 文件夹 + + + + Search syntax + 搜索语法 + + + + Search every comic and folder field, or build precise queries. + 搜索漫画和文件夹的所有字段,或构建精确查询。 + + + + Quick guide + 快速指南 + + + + Fields (%1) + 字段 (%1) + + + + Examples + 示例 + + + + Start with a simple search + 从简单搜索开始 + + + + Just start typing. Plain text search across all metadata. + 直接开始输入即可。纯文本会搜索所有元数据。 + + + + 1. Search everywhere + 1. 全局搜索 + + + + Type any text or quoted text. + 输入任意文本或带引号的文本。 + + + + 2. Target a field + 2. 指定字段 + + + + Use a field name followed by : or = + 使用字段名,后接 : 或 = + + + + 3. Combine conditions + 3. 组合条件 + + + + Use AND, OR, NOT and parentheses. + 使用 AND、OR、NOT 和括号。 + + + + Operators + 运算符 + + + + : or = + : 或 = + + + + contains the text + 包含该文本 + + + + matches the complete value + 匹配完整值 + + + + greater than / at least + 大于 / 至少 + + + + less than / at most + 小于 / 至多 + + + + "quoted text" + "带引号的文本" + + + + keeps spaces inside one value + 将空格保留在同一个值中 + + + + Dates and grouping + 日期和分组 + + + + added in the last 7 days + 在最近 7 天内添加 + + + + added more than 30 days ago + 在 30 多天前添加 + + + + group alternatives + 对备选条件分组 + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + 提示:空格等同于 AND,搜索不区分大小写。使用引号可在一个值中包含空格。 + + + + Find a field… + 查找字段… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + 文本可直接输入或放在引号中。整数字段支持 <、<=、> 和 >=。对于日期字段,整数表示天数(added>7 表示在最近 7 天内添加)。 + + + + Field + 字段 + + + + Description + 说明 + + + + Input + 输入 + + + + Example + 示例 + + + + Examples show the pattern—replace the values with your own. + 示例展示了格式,请将值替换为你自己的值。 + + + + Query + 查询 + + + + What it finds + 查找内容 + + + + Common filters + 常用筛选条件 + + + + Unread comics + 未读漫画 + + + + Comics in progress + 阅读中的漫画 + + + + Highly rated comics + 高评分漫画 + + + + Comics added in the last 7 days + 最近 7 天内添加的漫画 + + + + Metadata + 元数据 + + + + Search by series + 按系列搜索 + + + + Search by writer + 按编剧搜索 + + + + Manga comics + 日式漫画 + + + + Search textual tags + 搜索文本标签 + + + + Advanced combinations + 高级组合 + + + + Match either writer + 匹配任一编剧 + + + + Group alternatives + 对备选条件分组 + + + + Exclude a value + 排除某个值 + + + + Older, highly rated comics + 较早的高评分漫画 + + + + Copy query + 复制查询 + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + 空格等同于 AND。使用引号表示短语,使用括号控制分组。 + + SearchVolume @@ -2411,39 +3265,81 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - + + + Server connectivity + 服务器连接 + + + + Scan to connect + 扫描以连接 + + + + Devices on this network can reach your library at the address below. + 此网络中的设备可通过以下地址访问您的资料库。 + + + + IP address + IP 地址 + + + Port 端口 - - enable the server + + Web interface + 网页界面 + + + + Copy link + 复制链接 + + + + Open web UI + 打开网页界面 + + + + Enable the server 启用服务器 - + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader 支持 iOS 和 Android。获取 <a href='https://ios.yacreader.com'>iOS</a> 或 <a href='https://android.yacreader.com'>Android</a> 版本。 + + + enable the server + 启用服务器 + + Server connectivity information - 服务器连接信息 + 服务器连接信息 - Scan it! - 扫一扫! + 扫一扫! - - set port + + Set port + set port 设置端口 - Choose an IP address - 选择IP地址 + 选择IP地址 - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader 适用于 iOS 和 Android 设备。<br/>搜索 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. + YACReader 适用于 iOS 和 Android 设备。<br/>搜索 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. @@ -2466,7 +3362,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t issues - 发行 + @@ -2539,7 +3435,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Variant: - 变体: + 颜色设置: @@ -2554,7 +3450,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t Value - 价值 + @@ -2589,7 +3485,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t true - 真的 + true @@ -2597,7 +3493,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t false - 错误的 + false @@ -2715,7 +3611,7 @@ To stop an automatic update tap on the loading indicator next to the Libraries t issues - 发行 + @@ -2862,14 +3758,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + 发行说明不可用。 - + Previous versions - + 以前的版本 @@ -2905,22 +3801,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + Shortcuts 快捷键 - + + Keyboard shortcuts + 键盘快捷键 + + + + Customize the keyboard shortcuts used by the application. + 自定义应用程序使用的键盘快捷键。 + + + Edit shortcuts 编辑快捷键 @@ -2928,7 +3834,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + 搜索筛选条件 + + + type to search 搜索类型 diff --git a/YACReaderLibrary/yacreaderlibrary_zh_HK.ts b/YACReaderLibrary/yacreaderlibrary_zh_HK.ts index 430227e55..f01829b88 100644 --- a/YACReaderLibrary/yacreaderlibrary_zh_HK.ts +++ b/YACReaderLibrary/yacreaderlibrary_zh_HK.ts @@ -167,24 +167,24 @@ 無法載入目前主題 JSON。 - + Import theme 導入主題 - + JSON files (*.json);;All files (*) JSON 檔案 (*.json);;所有檔案 (*) - + Could not import theme from: %1 無法從以下位置匯入主題: %1 - + Could not import theme from: %1 @@ -195,7 +195,7 @@ %2 - + Import failed 導入失敗 @@ -294,67 +294,67 @@ ComicModel - + yes - + no - + Title 標題 - + File Name 檔案名 - + Pages 頁數 - + Size 大小 - + Read 閱讀 - + Current Page 當前頁 - + Publication Date 發行日期 - + Rating 評分 - + Series 系列 - + Volume 體積 - + Story Arc 故事線 @@ -619,25 +619,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + FolderContentView @@ -658,20 +663,25 @@ HelpAboutDialog - + About 關於 - + Help 幫助 - + System info 系統資訊 + + + Changelog + 更新日誌 + ImportComicsInfoDialog @@ -791,259 +801,289 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>正在掃描當前庫的舊版 XML metadata 資訊。</p><p>這只需要執行一次,且只有當創建庫的 YACReaderLibrary 版本低於 9.8.2 時。</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>正在檢查目前漫畫庫中遺失的封面及不完整的漫畫資訊。</p><p>這可能需要幾分鐘。你可以停止此程序,稍後再重新執行。</p> + LibraryWindow - + YACReader Library YACReader 庫 - + Library - + Set as read 設為已讀 - - + + Set as unread 設為未讀 - - - + + + manga 漫畫 - - - + + + comic 漫畫 - - - + + + web comic 網路漫畫 - - - + + + western manga (left to right) 西方漫畫(從左到右) - + Library not available Library ' 庫不可用 - + Rescan library for XML info 重新掃描庫的 XML 資訊 - + Delete folder 刪除檔夾 - + Open folder... 打開檔夾... - + Set as uncompleted 設為未完成 - + Set as completed 設為已完成 - + Update folder 更新檔夾 - + Folder 檔夾 - + Comic 漫畫 - + + A repair of this library is already running (%1). Wait for it to finish. + 此庫的修復已在執行中(%1)。請等待其完成。 + + + + The library is locked by a repair that did not finish. + 此庫已被一個未完成的修復鎖定。 + + + + The library is locked by a repair started by %1. + 此庫已被 %1 啟動的修復鎖定。 + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + 如果您確定沒有其他修復正在執行,可以移除該鎖定。移除鎖定並繼續? + + + Upgrade failed 更新失敗 - + There were errors during library upgrade in: 漫畫庫更新時出現錯誤: - + + Restore recovery failed + 還原復原失敗 + + + Update needed 需要更新 - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? 此庫是使用舊版本的YACReaderLibrary創建的. 它需要更新. 現在更新? - + Download new version 下載新版本 - + This library was created with a newer version of YACReaderLibrary. Download the new version now? 此庫是使用較新版本的YACReaderLibrary創建的。 立即下載新版本? - + Library '%1' is no longer available. Do you want to remove it? 庫 '%1' 不再可用。 你想刪除它嗎? - + Old library 舊的庫 - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? 庫 '%1' 是通過舊版本的YACReaderLibrary創建的。 必須再次創建。 你想現在創建嗎? - - + + Copying comics... 複製漫畫中... - - + + Moving comics... 移動漫畫中... - + Folder name: 檔夾名稱: - + No folder selected 沒有選中的檔夾 - + Please, select a folder first 請先選擇一個檔夾 - + Error in path 路徑錯誤 - + There was an error accessing the folder's path 訪問檔夾的路徑時出錯 - + The selected folder and all its contents will be deleted from your disk. Are you sure? 所選檔夾及其所有內容將從磁片中刪除。 你確定嗎? - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. 嘗試刪除所選檔夾時出現問題。 請檢查寫入許可權,並確保沒有其他應用程式在使用這些檔夾或檔。 - + Add new reading lists 添加新的閱讀列表 - - + + List name: 列表名稱: - + Delete list/label 刪除 列表/標籤 - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? 所選項目將被刪除,您的漫畫或檔夾將不會從您的磁片中刪除。 你確定嗎? - + Rename list name 重命名列表 - - - + + + 4koma (top to botom) 4koma(由上至下) - - - - + + + + Set type 套裝類型 - + Set custom cover 設定自訂封面 - + Delete custom cover 刪除自訂封面 - + Save covers 保存封面 - + You are adding too many libraries. 您添加的庫太多了。 - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1056,157 +1096,362 @@ YACReaderLibrary will not stop you from creating more libraries but you should k YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低的庫數量來提升性能。 - - + + YACReader not found YACReader 未找到 - + Error 錯誤 - + Error opening comic with third party reader. 使用第三方閱讀器開啟漫畫時出錯。 - + Library not found 未找到庫 - + The selected folder doesn't contain any library. 所選檔夾不包含任何庫。 - + Are you sure? 你確定嗎? - + Do you want remove 你想要刪除 - + library? 庫? - Remove and delete metadata - 移除並刪除元數據 + 移除並刪除元數據 - + Library info 圖書館資訊 - + Assign comics numbers 分配漫畫編號 - + Assign numbers starting in: 從以下位置開始分配編號: - - + + Unable to delete 無法刪除 - + + Search filters + 搜尋篩選器 + + + + Unread + 未讀 + + + + In progress + 閱讀中 + + + + Highly rated + 高評分 + + + + Recently added + 最近新增 + + + + Search syntax… + 搜尋語法… + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Add new folder 添加新的檔夾 - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. 未找到YACReader. YACReader應安裝在與YACReaderLibrary相同的檔夾中. - + YACReader not found. There might be a problem with your YACReader installation. 未找到YACReader. YACReader的安裝可能有問題. - + + + YACReader library database (*.ydb) + YACReader 漫畫庫資料庫 (*.ydb) + + + + The library database backup was created at: +%1 + 漫畫庫資料庫備份已建立於: +%1 + + + + Unable to create the library database backup: +%1 + 無法建立漫畫庫資料庫備份: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + 還原前請關閉 YACReaderLibraryServer 及正在使用此漫畫庫的所有其他 YACReader 應用程式。是否繼續? + + + + Restoring library database... + 正在還原漫畫庫資料庫... + + + + The current library database is invalid. Restore the selected backup anyway? + 目前的漫畫庫資料庫無效。仍要還原所選備份嗎? + + + + + The library maintenance lock may be stale. Remove it and retry? + 漫畫庫維護鎖可能已失效。是否移除並重試? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +再次嘗試復原前,請重新啟動 YACReaderLibrary。 + + + + The library database was restored successfully. Update the library now? + 漫畫庫資料庫已成功還原。是否立即更新漫畫庫? + + + + Library database damaged + 漫畫庫資料庫已損壞 + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + 漫畫庫「%1」的資料庫已損壞,因此無法執行一般更新、維護及備份。YACReader 可以嘗試修復資料庫。部分損壞的資料可能無法復原。現有備份不會被更改。 + + + + Attempt repair + 嘗試修復 + + + + Restore a backup... + 還原備份... + + + + Repairing library database... + 正在修復漫畫庫資料庫... + + + + + + Library database repair + 修復漫畫庫資料庫 + + + + Another maintenance operation is currently using this library. Try again after it finishes. + 另一個維護操作正在使用此漫畫庫。請在操作完成後重試。 + + + + The library database is already valid. + 漫畫庫資料庫已經有效。 + + + + Library database repaired + 漫畫庫資料庫已修復 + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + 已透過重建索引修復漫畫庫資料庫。損壞的原始檔案已保留於: +%1 + + + + Library database rebuilt + 漫畫庫資料庫已重建 + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + 漫畫庫資料庫已成功重建。損壞的原始檔案已保留於: +%1 + +是否立即更新漫畫庫? + + + + + +The damaged original was preserved at: +%1 + + +損壞的原始檔案已保留於: +%1 + + + + Library database repair failed + 漫畫庫資料庫修復失敗 + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + 無法修復漫畫庫資料庫: +%1%2 + +您可以從「漫畫庫」選單還原備份,或重新建立漫畫庫。 + + + + Remove and delete metadata and backups + 移除並刪除中繼資料及備份 + + + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. 嘗試刪除所選漫畫時出現問題。 請檢查所選檔或包含檔夾中的寫入許可權。 - + Invalid image 圖片無效 - + The selected file is not a valid image. 所選檔案不是有效影像。 - + Error saving cover 儲存封面時發生錯誤 - + There was an error saving the cover image. 儲存封面圖片時發生錯誤。 - + Error creating the library 創建庫時出錯 - + Error updating the library 更新庫時出錯 - + Error opening the library 打開庫時出錯 - + Delete comics 刪除漫畫 - + All the selected comics will be deleted from your disk. Are you sure? 所有選定的漫畫都將從您的磁片中刪除。你確定嗎? - + Remove comics 移除漫畫 - + Comics will only be deleted from the current label/list. Are you sure? 漫畫只會從當前標籤/列表中刪除。 你確定嗎? - + Library name already exists 庫名已存在 - + There is another library with the name '%1'. 已存在另一個名為'%1'的庫。 + + + Repaired: %1 +Failed: %2 +Missing files: %3 + 已修復:%1 +失敗:%2 +遺失檔案:%3 + LibraryWindowActions @@ -1264,384 +1509,425 @@ YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低 + Back up library database + 備份漫畫庫資料庫 + + + + Create a backup of the current library database + 建立目前漫畫庫資料庫的備份 + + + + Restore library database backup + 還原漫畫庫資料庫備份 + + + + Restore the current library database from a backup + 從備份還原目前的漫畫庫資料庫 + + + + + Repair covers and comic info + 修復封面及漫畫資訊 + + + + Retry comics with missing covers or incomplete information + 重新處理缺少封面或資訊不完整的漫畫 + + + Rename library 重命名庫 - + Rename current library 重命名當前庫 - + Remove library 移除庫 - + Remove current library from your collection 從您的集合中移除當前庫 - + Rescan library for XML info 重新掃描庫的 XML 資訊 - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. 嘗試查找漫畫檔內嵌的 XML 資訊。只有當創建庫的 YACReaderLibrary 版本低於 9.8.2 或者使用第三方軟體嵌入 XML 資訊時,才需要執行該操作。 - + + Open library folder... + 打開庫檔夾... + + + + Open the root folder of the current library + 打開目前庫的根檔夾 + + + Show library info 顯示圖書館資訊 - + Show information about the current library 顯示當前庫的信息 - + Open current comic 打開當前漫畫 - + Open current comic on YACReader 用YACReader打開漫畫 - + Save selected covers to... 選中的封面保存到... - + Save covers of the selected comics as JPG files 保存所選的封面為jpg - - + + Set as read 設為已讀 - + Set comic as read 漫畫設為已讀 - - + + Set as unread 設為未讀 - + Set comic as unread 漫畫設為未讀 - - + + manga 漫畫 - + Set issue as manga 將問題設定為漫畫 - - + + comic 漫畫 - + Set issue as normal 設置發行狀態為正常發行 - + western manga 西方漫畫 - + Set issue as western manga 將問題設定為西方漫畫 - - + + web comic 網路漫畫 - + Set issue as web comic 將問題設定為網路漫畫 - - + + yonkoma 四科馬 - + Set issue as yonkoma 將問題設定為 yonkoma - + Show/Hide marks 顯示/隱藏標記 - + Show or hide read marks 顯示或隱藏閱讀標記 - + Show/Hide recent indicator 顯示/隱藏最近的指標 - + Show or hide recent indicator 顯示或隱藏最近的指示器 - - + + Fullscreen mode on/off 全屏模式 開/關 - + Help, About YACReader 幫助, 關於 YACReader - + Add new folder 添加新的檔夾 - + Add new folder to the current library 在當前庫下添加新的檔夾 - + Delete folder 刪除檔夾 - + Delete current folder from disk 從磁片上刪除當前檔夾 - + Select root node 選擇根節點 - + Expand all nodes 展開所有節點 - + Collapse all nodes 折疊所有節點 - + Show options dialog 顯示選項對話框 - + Show comics server options dialog 顯示漫畫伺服器選項對話框 - - + + Change between comics views 漫畫視圖之間的變化 - + Open folder... 打開檔夾... - + Set as uncompleted 設為未完成 - + Set as completed 設為已完成 - + Set custom cover 設定自訂封面 - + Delete custom cover 刪除自訂封面 - + western manga (left to right) 西方漫畫(從左到右) - + Open containing folder... 打開包含檔夾... - + Reset comic rating 重置漫畫評分 - + Select all comics 全選漫畫 - + Edit 編輯 - + Assign current order to comics 將當前序號分配給漫畫 - + Update cover 更新封面 - + Delete selected comics 刪除所選的漫畫 - + Delete metadata from selected comics 從選定的漫畫中刪除元數據 - + Download tags from Comic Vine 從 Comic Vine 下載標籤 - + Focus search line 聚焦於搜索行 - + Focus comics view 聚焦於漫畫視圖 - + Edit shortcuts 編輯快捷鍵 - + &Quit 退出(&Q) - + Update folder 更新檔夾 - + Update current folder 更新當前檔夾 - + Scan legacy XML metadata 掃描舊版 XML 元數據 - + Add new reading list 添加新的閱讀列表 - + Add a new reading list to the current library 在當前庫添加新的閱讀列表 - + Remove reading list 移除閱讀列表 - + Remove current reading list from the library 從當前庫移除閱讀列表 - + Add new label 添加新標籤 - + Add a new label to this library 在當前庫添加標籤 - + Rename selected list 重命名列表 - + Rename any selected labels or lists 重命名任何選定的標籤或列表 - + Add to... 添加到... - + Favorites 收藏夾 - + Add selected comics to favorites list 將所選漫畫添加到收藏夾列表 @@ -1688,73 +1974,73 @@ YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低 OptionsDialog - + Language 語言 - + Application language 應用程式語言 - + System default 系統預設 - + Tray icon settings (experimental) 託盤圖示設置 (實驗特性) - + Close to tray 關閉至託盤 - + Start into the system tray 啟動至系統託盤 - + Edit Comic Vine API key 編輯Comic Vine API 密匙 - + Comic Vine API key Comic Vine API 密匙 - + ComicInfo.xml legacy support ComicInfo.xml 遺留支持 - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics 新增漫畫時從 ComicInfo.xml 匯入元數據 - + Consider 'recent' items added or updated since X days ago 考慮自 X 天前新增或更新的「最近」項目 - + Third party reader 第三方閱讀器 - + Write {comic_file_path} where the path should go in the command 在命令中應將路徑寫入 {comic_file_path} - + Clear 清空 @@ -1919,12 +2205,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 外貌 - + Options 選項 - + Restart is needed 需要重啟 @@ -2295,6 +2581,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 第 %1 頁 共: %2 條 + + SearchFieldRegistry + + + Text, quoted text + 文字、引號內文字 + + + + Integer + 整數 + + + + Boolean (true / false) + 布林值 (true / false) + + + + Integer (number of days) + 整數(日數) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + 列舉 (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + 漫畫標題 + + + + Series name + 系列名稱 + + + + Issue number + 期號 + + + + Volume identifier + 卷冊識別碼 + + + + Reading format + 閱讀格式 + + + + Comic rating + 漫畫評分 + + + + Textual tags + 文字標籤 + + + + Writer credit + 編劇署名 + + + + Penciller credit + 鉛筆畫師署名 + + + + Inker credit + 墨線畫師署名 + + + + Colorist credit + 上色師署名 + + + + Letterer credit + 嵌字師署名 + + + + Cover artist credit + 封面畫師署名 + + + + Editor credit + 編輯署名 + + + + Story arc name + 故事線名稱 + + + + Position within a story arc + 故事線內的位置 + + + + Number of issues in a story arc + 故事線內的期數 + + + + Characters appearing in the comic + 漫畫中出現的角色 + + + + Teams appearing in the comic + 漫畫中出現的團隊 + + + + Locations appearing in the comic + 漫畫中出現的地點 + + + + Primary character or team + 主要角色或團隊 + + + + Comic synopsis + 漫畫簡介 + + + + Publisher name + 出版社名稱 + + + + Publishing imprint + 出版品牌 + + + + Publication format + 出版格式 + + + + Recommended age rating + 建議年齡分級 + + + + Comic genre + 漫畫類型 + + + + ISO language code + ISO 語言代碼 + + + + Publication date metadata + 出版日期元資料 + + + + Series grouping metadata + 系列分組元資料 + + + + Alternate series name + 其他系列名稱 + + + + Alternate issue number + 其他期號 + + + + Alternate series issue count + 其他系列期數 + + + + Number of issues in the series + 系列期數 + + + + Whether the comic is marked as read + 漫畫是否標記為已讀 + + + + Whether reading has started + 是否已開始閱讀 + + + + Whether metadata has been edited + 元資料是否已編輯 + + + + Whether the comic is in color + 漫畫是否為彩色 + + + + Number of pages + 頁數 + + + + Comic file name + 漫畫檔案名稱 + + + + When the item was added + 項目的新增時間 + + + + When the comic was last opened + 漫畫上次開啟時間 + + + + Comic notes + 漫畫備註 + + + + Review text + 評論文字 + + + + Parent folder name + 上層資料夾名稱 + + + + Default reading format for the folder + 資料夾的預設閱讀格式 + + + + Whether the folder is complete + 資料夾是否完整 + + + + Whether the folder is marked as finished + 資料夾是否標記為已完結 + + + + When the folder was updated + 資料夾的更新時間 + + SearchSingleComic @@ -2314,6 +2873,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 使用精確匹配搜尋。如果您想要尋找與名稱中某些單字相符的捲,請停用。 + + SearchSyntaxDialog + + + Common + 常用 + + + + Credits + 創作人員 + + + + Story + 故事 + + + + Publication + 出版 + + + + Reading & files + 閱讀與檔案 + + + + Folders + 資料夾 + + + + Search syntax + 搜尋語法 + + + + Search every comic and folder field, or build precise queries. + 搜尋漫畫及資料夾的所有欄位,或建立精確查詢。 + + + + Quick guide + 快速指南 + + + + Fields (%1) + 欄位 (%1) + + + + Examples + 範例 + + + + Start with a simple search + 由簡單搜尋開始 + + + + Just start typing. Plain text search across all metadata. + 直接開始輸入即可。純文字會搜尋所有元資料。 + + + + 1. Search everywhere + 1. 全域搜尋 + + + + Type any text or quoted text. + 輸入任何文字或引號內文字。 + + + + 2. Target a field + 2. 指定欄位 + + + + Use a field name followed by : or = + 使用欄位名稱,後接 : 或 = + + + + 3. Combine conditions + 3. 組合條件 + + + + Use AND, OR, NOT and parentheses. + 使用 AND、OR、NOT 及括號。 + + + + Operators + 運算子 + + + + : or = + : 或 = + + + + contains the text + 包含該文字 + + + + matches the complete value + 符合完整值 + + + + greater than / at least + 大於 / 至少 + + + + less than / at most + 小於 / 至多 + + + + "quoted text" + "引號內文字" + + + + keeps spaces inside one value + 將空格保留在同一個值內 + + + + Dates and grouping + 日期及分組 + + + + added in the last 7 days + 在最近 7 日內新增 + + + + added more than 30 days ago + 在 30 多日前新增 + + + + group alternatives + 將替代條件分組 + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + 提示:空格等同於 AND,搜尋不區分大小寫。使用引號可在一個值內包含空格。 + + + + Find a field… + 尋找欄位… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + 文字可直接輸入或放在引號內。整數欄位支援 <、<=、> 及 >=。在日期欄位中,整數表示日數(added>7 表示在最近 7 日內新增)。 + + + + Field + 欄位 + + + + Description + 說明 + + + + Input + 輸入 + + + + Example + 範例 + + + + Examples show the pattern—replace the values with your own. + 範例會顯示格式;請將值換成你自己的值。 + + + + Query + 查詢 + + + + What it finds + 尋找內容 + + + + Common filters + 常用篩選器 + + + + Unread comics + 未讀漫畫 + + + + Comics in progress + 閱讀中的漫畫 + + + + Highly rated comics + 高評分漫畫 + + + + Comics added in the last 7 days + 最近 7 日內新增的漫畫 + + + + Metadata + 元資料 + + + + Search by series + 按系列搜尋 + + + + Search by writer + 按編劇搜尋 + + + + Manga comics + 日式漫畫 + + + + Search textual tags + 搜尋文字標籤 + + + + Advanced combinations + 進階組合 + + + + Match either writer + 符合其中一位編劇 + + + + Group alternatives + 將替代條件分組 + + + + Exclude a value + 排除某個值 + + + + Older, highly rated comics + 較舊的高評分漫畫 + + + + Copy query + 複製查詢 + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + 空格等同於 AND。使用引號表示詞組,使用括號控制分組。 + + SearchVolume @@ -2419,40 +3273,82 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - - set port - 設置端口 + + Set port + set port + 設定連接埠 - Server connectivity information - 伺服器連接資訊 + 伺服器連接資訊 - Scan it! - 掃一掃! + 掃一掃! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader 適用於 iOS 和 Android 裝置。 <br/>發現它適用於 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 或 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>。 + YACReader 適用於 iOS 和 Android 裝置。 <br/>發現它適用於 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 或 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>。 - Choose an IP address - 選擇IP地址 + 選擇IP地址 + + + + + Server connectivity + 伺服器連線 + + + + Scan to connect + 掃描以連線 + + + + Devices on this network can reach your library at the address below. + 此網絡中的裝置可透過以下地址存取你的資料庫。 - + + IP address + IP 地址 + + + Port 端口 - - enable the server + + Web interface + 網頁介面 + + + + Copy link + 複製連結 + + + + Open web UI + 開啟網頁介面 + + + + Enable the server 啟用伺服器 + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader 支援 iOS 及 Android。取得 <a href='https://ios.yacreader.com'>iOS</a> 或 <a href='https://android.yacreader.com'>Android</a> 版本。 + + + enable the server + 啟用伺服器 + SortVolumeComics @@ -2870,14 +3766,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + 版本資訊無法使用。 - + Previous versions - + 舊版本 @@ -2913,22 +3809,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + + Keyboard shortcuts + 鍵盤快捷鍵 + + + + Customize the keyboard shortcuts used by the application. + 自訂應用程式使用的鍵盤快捷鍵。 + + + Edit shortcuts 編輯快捷鍵 - + Shortcuts 快捷鍵 @@ -2936,7 +3842,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + 搜尋篩選器 + + + type to search 搜索類型 diff --git a/YACReaderLibrary/yacreaderlibrary_zh_TW.ts b/YACReaderLibrary/yacreaderlibrary_zh_TW.ts index 17b440134..8eae882f7 100644 --- a/YACReaderLibrary/yacreaderlibrary_zh_TW.ts +++ b/YACReaderLibrary/yacreaderlibrary_zh_TW.ts @@ -167,24 +167,24 @@ 無法載入目前主題 JSON。 - + Import theme 導入主題 - + JSON files (*.json);;All files (*) JSON 檔案 (*.json);;所有檔案 (*) - + Could not import theme from: %1 無法從以下位置匯入主題: %1 - + Could not import theme from: %1 @@ -195,7 +195,7 @@ %2 - + Import failed 導入失敗 @@ -294,67 +294,67 @@ ComicModel - + yes - + no - + Title 標題 - + File Name 檔案名 - + Pages 頁數 - + Size 大小 - + Read 閱讀 - + Current Page 當前頁 - + Publication Date 發行日期 - + Rating 評分 - + Series 系列 - + Volume 體積 - + Story Arc 故事線 @@ -619,25 +619,30 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + FolderContentView @@ -658,20 +663,25 @@ HelpAboutDialog - + About 關於 - + Help 幫助 - + System info 系統資訊 + + + Changelog + 更新日誌 + ImportComicsInfoDialog @@ -791,259 +801,289 @@ <p>Current library is being scanned for legacy XML metadata information.</p><p>This is only needed once, and only if the library was crated with YACReaderLibrary 9.8.2 or earlier.</p> <p>正在掃描當前庫的舊版 XML metadata 資訊。</p><p>這只需要執行一次,且只有當創建庫的 YACReaderLibrary 版本低於 9.8.2 時。</p> + + + <p>The current library is being checked for missing covers and incomplete comic information.</p><p>This can take several minutes. You can stop the process and run it again later.</p> + <p>正在檢查目前漫畫庫中遺失的封面和不完整的漫畫資訊。</p><p>這可能需要幾分鐘。您可以停止此程序,稍後再重新執行。</p> + LibraryWindow - + YACReader Library YACReader 庫 - + Library - + Set as read 設為已讀 - - + + Set as unread 設為未讀 - - - + + + manga 漫畫 - - - + + + comic 漫畫 - - - + + + web comic 網路漫畫 - - - + + + western manga (left to right) 西方漫畫(從左到右) - + Library not available Library ' 庫不可用 - + Rescan library for XML info 重新掃描庫的 XML 資訊 - + Delete folder 刪除檔夾 - + Open folder... 打開檔夾... - + Set as uncompleted 設為未完成 - + Set as completed 設為已完成 - + Update folder 更新檔夾 - + Folder 檔夾 - + Comic 漫畫 - + + A repair of this library is already running (%1). Wait for it to finish. + 此庫的修復已在執行中(%1)。請等待其完成。 + + + + The library is locked by a repair that did not finish. + 此庫已被一個未完成的修復鎖定。 + + + + The library is locked by a repair started by %1. + 此庫已被 %1 啟動的修復鎖定。 + + + + If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue? + 如果您確定沒有其他修復正在執行,可以移除該鎖定。移除鎖定並繼續? + + + Upgrade failed 更新失敗 - + There were errors during library upgrade in: 漫畫庫更新時出現錯誤: - + + Restore recovery failed + 還原復原失敗 + + + Update needed 需要更新 - + This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now? 此庫是使用舊版本的YACReaderLibrary創建的. 它需要更新. 現在更新? - + Download new version 下載新版本 - + This library was created with a newer version of YACReaderLibrary. Download the new version now? 此庫是使用較新版本的YACReaderLibrary創建的。 立即下載新版本? - + Library '%1' is no longer available. Do you want to remove it? 庫 '%1' 不再可用。 你想刪除它嗎? - + Old library 舊的庫 - + Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now? 庫 '%1' 是通過舊版本的YACReaderLibrary創建的。 必須再次創建。 你想現在創建嗎? - - + + Copying comics... 複製漫畫中... - - + + Moving comics... 移動漫畫中... - + Folder name: 檔夾名稱: - + No folder selected 沒有選中的檔夾 - + Please, select a folder first 請先選擇一個檔夾 - + Error in path 路徑錯誤 - + There was an error accessing the folder's path 訪問檔夾的路徑時出錯 - + The selected folder and all its contents will be deleted from your disk. Are you sure? 所選檔夾及其所有內容將從磁片中刪除。 你確定嗎? - + There was an issue trying to delete the selected folders. Please, check for write permissions and be sure that any applications are using these folders or any of the contained files. 嘗試刪除所選檔夾時出現問題。 請檢查寫入許可權,並確保沒有其他應用程式在使用這些檔夾或檔。 - + Add new reading lists 添加新的閱讀列表 - - + + List name: 列表名稱: - + Delete list/label 刪除 列表/標籤 - + The selected item will be deleted, your comics or folders will NOT be deleted from your disk. Are you sure? 所選項目將被刪除,您的漫畫或檔夾將不會從您的磁片中刪除。 你確定嗎? - + Rename list name 重命名列表 - - - + + + 4koma (top to botom) 4koma(由上至下) - - - - + + + + Set type 套裝類型 - + Set custom cover 設定自訂封面 - + Delete custom cover 刪除自訂封面 - + Save covers 保存封面 - + You are adding too many libraries. 您添加的庫太多了。 - + You are adding too many libraries. You probably only need one library in your top level comics folder, you can browse any subfolders using the folders section in the left sidebar. @@ -1056,157 +1096,362 @@ YACReaderLibrary will not stop you from creating more libraries but you should k YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低的庫數量來提升性能。 - - + + YACReader not found YACReader 未找到 - + Error 錯誤 - + Error opening comic with third party reader. 使用第三方閱讀器開啟漫畫時出錯。 - + Library not found 未找到庫 - + The selected folder doesn't contain any library. 所選檔夾不包含任何庫。 - + Are you sure? 你確定嗎? - + Do you want remove 你想要刪除 - + library? 庫? - Remove and delete metadata - 移除並刪除元數據 + 移除並刪除元數據 - + Library info 圖書館資訊 - + Assign comics numbers 分配漫畫編號 - + Assign numbers starting in: 從以下位置開始分配編號: - - + + Unable to delete 無法刪除 - + + Search filters + 搜尋篩選條件 + + + + Unread + 未讀 + + + + In progress + 閱讀中 + + + + Highly rated + 高評分 + + + + Recently added + 最近加入 + + + + Search syntax… + 搜尋語法… + + + + Package operation failed + + + + + The covers package operation could not be completed. + + + + Add new folder 添加新的檔夾 - + YACReader not found. YACReader should be installed in the same folder as YACReaderLibrary. 未找到YACReader. YACReader應安裝在與YACReaderLibrary相同的檔夾中. - + YACReader not found. There might be a problem with your YACReader installation. 未找到YACReader. YACReader的安裝可能有問題. - + + + YACReader library database (*.ydb) + YACReader 漫畫庫資料庫 (*.ydb) + + + + The library database backup was created at: +%1 + 漫畫庫資料庫備份已建立於: +%1 + + + + Unable to create the library database backup: +%1 + 無法建立漫畫庫資料庫備份: +%1 + + + + Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue? + 還原前請關閉 YACReaderLibraryServer 以及正在使用此漫畫庫的所有其他 YACReader 應用程式。是否繼續? + + + + Restoring library database... + 正在還原漫畫庫資料庫... + + + + The current library database is invalid. Restore the selected backup anyway? + 目前的漫畫庫資料庫無效。仍要還原所選備份嗎? + + + + + The library maintenance lock may be stale. Remove it and retry? + 漫畫庫維護鎖可能已失效。是否移除並重試? + + + + + +Restart YACReaderLibrary before attempting recovery again. + + +再次嘗試復原前,請重新啟動 YACReaderLibrary。 + + + + The library database was restored successfully. Update the library now? + 漫畫庫資料庫已成功還原。是否立即更新漫畫庫? + + + + Library database damaged + 漫畫庫資料庫已損壞 + + + + The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed. + 漫畫庫「%1」的資料庫已損壞,因此無法執行一般更新、維護與備份。YACReader 可以嘗試修復資料庫。部分損壞的資料可能無法復原。現有備份不會被變更。 + + + + Attempt repair + 嘗試修復 + + + + Restore a backup... + 還原備份... + + + + Repairing library database... + 正在修復漫畫庫資料庫... + + + + + + Library database repair + 修復漫畫庫資料庫 + + + + Another maintenance operation is currently using this library. Try again after it finishes. + 另一個維護操作正在使用此漫畫庫。請在操作完成後重試。 + + + + The library database is already valid. + 漫畫庫資料庫已經有效。 + + + + Library database repaired + 漫畫庫資料庫已修復 + + + + The library database was repaired by rebuilding its indexes. The damaged original was preserved at: +%1 + 已透過重建索引修復漫畫庫資料庫。損壞的原始檔案已保留於: +%1 + + + + Library database rebuilt + 漫畫庫資料庫已重建 + + + + The library database was rebuilt successfully. The damaged original was preserved at: +%1 + +Update the library now? + 漫畫庫資料庫已成功重建。損壞的原始檔案已保留於: +%1 + +是否立即更新漫畫庫? + + + + + +The damaged original was preserved at: +%1 + + +損壞的原始檔案已保留於: +%1 + + + + Library database repair failed + 漫畫庫資料庫修復失敗 + + + + The library database could not be repaired: +%1%2 + +You can restore a backup from the Library menu or recreate the library. + 無法修復漫畫庫資料庫: +%1%2 + +您可以從「漫畫庫」選單還原備份,或重新建立漫畫庫。 + + + + Remove and delete metadata and backups + 移除並刪除中繼資料與備份 + + + There was an issue trying to delete the selected comics. Please, check for write permissions in the selected files or containing folder. 嘗試刪除所選漫畫時出現問題。 請檢查所選檔或包含檔夾中的寫入許可權。 - + Invalid image 圖片無效 - + The selected file is not a valid image. 所選檔案不是有效影像。 - + Error saving cover 儲存封面時發生錯誤 - + There was an error saving the cover image. 儲存封面圖片時發生錯誤。 - + Error creating the library 創建庫時出錯 - + Error updating the library 更新庫時出錯 - + Error opening the library 打開庫時出錯 - + Delete comics 刪除漫畫 - + All the selected comics will be deleted from your disk. Are you sure? 所有選定的漫畫都將從您的磁片中刪除。你確定嗎? - + Remove comics 移除漫畫 - + Comics will only be deleted from the current label/list. Are you sure? 漫畫只會從當前標籤/列表中刪除。 你確定嗎? - + Library name already exists 庫名已存在 - + There is another library with the name '%1'. 已存在另一個名為'%1'的庫。 + + + Repaired: %1 +Failed: %2 +Missing files: %3 + 已修復:%1 +失敗:%2 +遺失檔案:%3 + LibraryWindowActions @@ -1264,384 +1509,425 @@ YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低 + Back up library database + 備份漫畫庫資料庫 + + + + Create a backup of the current library database + 建立目前漫畫庫資料庫的備份 + + + + Restore library database backup + 還原漫畫庫資料庫備份 + + + + Restore the current library database from a backup + 從備份還原目前的漫畫庫資料庫 + + + + + Repair covers and comic info + 修復封面與漫畫資訊 + + + + Retry comics with missing covers or incomplete information + 重新處理缺少封面或資訊不完整的漫畫 + + + Rename library 重命名庫 - + Rename current library 重命名當前庫 - + Remove library 移除庫 - + Remove current library from your collection 從您的集合中移除當前庫 - + Rescan library for XML info 重新掃描庫的 XML 資訊 - + Tries to find XML info embedded in comic files. You only need to do this if the library was created with 9.8.2 or earlier versions or if you are using third party software to embed XML info in the files. 嘗試查找漫畫檔內嵌的 XML 資訊。只有當創建庫的 YACReaderLibrary 版本低於 9.8.2 或者使用第三方軟體嵌入 XML 資訊時,才需要執行該操作。 - + + Open library folder... + 開啟資料庫資料夾... + + + + Open the root folder of the current library + 開啟目前資料庫的根資料夾 + + + Show library info 顯示圖書館資訊 - + Show information about the current library 顯示當前庫的信息 - + Open current comic 打開當前漫畫 - + Open current comic on YACReader 用YACReader打開漫畫 - + Save selected covers to... 選中的封面保存到... - + Save covers of the selected comics as JPG files 保存所選的封面為jpg - - + + Set as read 設為已讀 - + Set comic as read 漫畫設為已讀 - - + + Set as unread 設為未讀 - + Set comic as unread 漫畫設為未讀 - - + + manga 漫畫 - + Set issue as manga 將問題設定為漫畫 - - + + comic 漫畫 - + Set issue as normal 設置發行狀態為正常發行 - + western manga 西方漫畫 - + Set issue as western manga 將問題設定為西方漫畫 - - + + web comic 網路漫畫 - + Set issue as web comic 將問題設定為網路漫畫 - - + + yonkoma 四科馬 - + Set issue as yonkoma 將問題設定為 yonkoma - + Show/Hide marks 顯示/隱藏標記 - + Show or hide read marks 顯示或隱藏閱讀標記 - + Show/Hide recent indicator 顯示/隱藏最近的指標 - + Show or hide recent indicator 顯示或隱藏最近的指示器 - - + + Fullscreen mode on/off 全屏模式 開/關 - + Help, About YACReader 幫助, 關於 YACReader - + Add new folder 添加新的檔夾 - + Add new folder to the current library 在當前庫下添加新的檔夾 - + Delete folder 刪除檔夾 - + Delete current folder from disk 從磁片上刪除當前檔夾 - + Select root node 選擇根節點 - + Expand all nodes 展開所有節點 - + Collapse all nodes 折疊所有節點 - + Show options dialog 顯示選項對話框 - + Show comics server options dialog 顯示漫畫伺服器選項對話框 - - + + Change between comics views 漫畫視圖之間的變化 - + Open folder... 打開檔夾... - + Set as uncompleted 設為未完成 - + Set as completed 設為已完成 - + Set custom cover 設定自訂封面 - + Delete custom cover 刪除自訂封面 - + western manga (left to right) 西方漫畫(從左到右) - + Open containing folder... 打開包含檔夾... - + Reset comic rating 重置漫畫評分 - + Select all comics 全選漫畫 - + Edit 編輯 - + Assign current order to comics 將當前序號分配給漫畫 - + Update cover 更新封面 - + Delete selected comics 刪除所選的漫畫 - + Delete metadata from selected comics 從選定的漫畫中刪除元數據 - + Download tags from Comic Vine 從 Comic Vine 下載標籤 - + Focus search line 聚焦於搜索行 - + Focus comics view 聚焦於漫畫視圖 - + Edit shortcuts 編輯快捷鍵 - + &Quit 退出(&Q) - + Update folder 更新檔夾 - + Update current folder 更新當前檔夾 - + Scan legacy XML metadata 掃描舊版 XML 元數據 - + Add new reading list 添加新的閱讀列表 - + Add a new reading list to the current library 在當前庫添加新的閱讀列表 - + Remove reading list 移除閱讀列表 - + Remove current reading list from the library 從當前庫移除閱讀列表 - + Add new label 添加新標籤 - + Add a new label to this library 在當前庫添加標籤 - + Rename selected list 重命名列表 - + Rename any selected labels or lists 重命名任何選定的標籤或列表 - + Add to... 添加到... - + Favorites 收藏夾 - + Add selected comics to favorites list 將所選漫畫添加到收藏夾列表 @@ -1688,73 +1974,73 @@ YACReaderLibrary不會阻止您創建更多的庫,但是您應該保持較低 OptionsDialog - + Language 語言 - + Application language 應用程式語言 - + System default 系統預設 - + Tray icon settings (experimental) 託盤圖示設置 (實驗特性) - + Close to tray 關閉至託盤 - + Start into the system tray 啟動至系統託盤 - + Edit Comic Vine API key 編輯Comic Vine API 密匙 - + Comic Vine API key Comic Vine API 密匙 - + ComicInfo.xml legacy support ComicInfo.xml 遺留支持 - + Import metadata from ComicInfo.xml when adding new comics Import metada from ComicInfo.xml when adding new comics 新增漫畫時從 ComicInfo.xml 匯入元數據 - + Consider 'recent' items added or updated since X days ago 考慮自 X 天前新增或更新的「最近」項目 - + Third party reader 第三方閱讀器 - + Write {comic_file_path} where the path should go in the command 在命令中應將路徑寫入 {comic_file_path} - + Clear 清空 @@ -1919,12 +2205,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 外貌 - + Options 選項 - + Restart is needed 需要重啟 @@ -2295,6 +2581,279 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 第 %1 頁 共: %2 條 + + SearchFieldRegistry + + + Text, quoted text + 文字、加上引號的文字 + + + + Integer + 整數 + + + + Boolean (true / false) + 布林值 (true / false) + + + + Integer (number of days) + 整數(天數) + + + + Enum (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + 列舉 (comic, manga, westernmanga, webcomic/web, 4koma/yonkoma) + + + + Comic title + 漫畫標題 + + + + Series name + 系列名稱 + + + + Issue number + 期號 + + + + Volume identifier + 卷冊識別碼 + + + + Reading format + 閱讀格式 + + + + Comic rating + 漫畫評分 + + + + Textual tags + 文字標籤 + + + + Writer credit + 編劇署名 + + + + Penciller credit + 鉛筆畫師署名 + + + + Inker credit + 墨線畫師署名 + + + + Colorist credit + 上色師署名 + + + + Letterer credit + 嵌字師署名 + + + + Cover artist credit + 封面畫師署名 + + + + Editor credit + 編輯署名 + + + + Story arc name + 故事線名稱 + + + + Position within a story arc + 故事線中的位置 + + + + Number of issues in a story arc + 故事線中的期數 + + + + Characters appearing in the comic + 漫畫中出現的角色 + + + + Teams appearing in the comic + 漫畫中出現的團隊 + + + + Locations appearing in the comic + 漫畫中出現的地點 + + + + Primary character or team + 主要角色或團隊 + + + + Comic synopsis + 漫畫簡介 + + + + Publisher name + 出版社名稱 + + + + Publishing imprint + 出版品牌 + + + + Publication format + 出版格式 + + + + Recommended age rating + 建議年齡分級 + + + + Comic genre + 漫畫類型 + + + + ISO language code + ISO 語言代碼 + + + + Publication date metadata + 出版日期中繼資料 + + + + Series grouping metadata + 系列分組中繼資料 + + + + Alternate series name + 替代系列名稱 + + + + Alternate issue number + 替代期號 + + + + Alternate series issue count + 替代系列期數 + + + + Number of issues in the series + 系列期數 + + + + Whether the comic is marked as read + 漫畫是否標示為已讀 + + + + Whether reading has started + 是否已開始閱讀 + + + + Whether metadata has been edited + 中繼資料是否已編輯 + + + + Whether the comic is in color + 漫畫是否為彩色 + + + + Number of pages + 頁數 + + + + Comic file name + 漫畫檔案名稱 + + + + When the item was added + 項目的加入時間 + + + + When the comic was last opened + 漫畫上次開啟時間 + + + + Comic notes + 漫畫備註 + + + + Review text + 評論文字 + + + + Parent folder name + 上層資料夾名稱 + + + + Default reading format for the folder + 資料夾的預設閱讀格式 + + + + Whether the folder is complete + 資料夾是否完整 + + + + Whether the folder is marked as finished + 資料夾是否標示為已完結 + + + + When the folder was updated + 資料夾的更新時間 + + SearchSingleComic @@ -2314,6 +2873,301 @@ To stop an automatic update tap on the loading indicator next to the Libraries t 使用精確匹配搜尋。如果您想要尋找與名稱中某些單字相符的捲,請停用。 + + SearchSyntaxDialog + + + Common + 常用 + + + + Credits + 創作人員 + + + + Story + 故事 + + + + Publication + 出版 + + + + Reading & files + 閱讀與檔案 + + + + Folders + 資料夾 + + + + Search syntax + 搜尋語法 + + + + Search every comic and folder field, or build precise queries. + 搜尋漫畫與資料夾的所有欄位,或建立精確查詢。 + + + + Quick guide + 快速指南 + + + + Fields (%1) + 欄位 (%1) + + + + Examples + 範例 + + + + Start with a simple search + 從簡單搜尋開始 + + + + Just start typing. Plain text search across all metadata. + 直接開始輸入即可。純文字會搜尋所有中繼資料。 + + + + 1. Search everywhere + 1. 全域搜尋 + + + + Type any text or quoted text. + 輸入任何文字或加上引號的文字。 + + + + 2. Target a field + 2. 指定欄位 + + + + Use a field name followed by : or = + 使用欄位名稱,後接 : 或 = + + + + 3. Combine conditions + 3. 組合條件 + + + + Use AND, OR, NOT and parentheses. + 使用 AND、OR、NOT 與括號。 + + + + Operators + 運算子 + + + + : or = + : 或 = + + + + contains the text + 包含該文字 + + + + matches the complete value + 符合完整值 + + + + greater than / at least + 大於 / 至少 + + + + less than / at most + 小於 / 至多 + + + + "quoted text" + "加上引號的文字" + + + + keeps spaces inside one value + 將空格保留在同一個值中 + + + + Dates and grouping + 日期與分組 + + + + added in the last 7 days + 在最近 7 天內加入 + + + + added more than 30 days ago + 在 30 多天前加入 + + + + group alternatives + 將替代條件分組 + + + + Tips: +Spaces act like AND, and searches are not case-sensitive. +Use quotes to include spaces in a value. + 提示:空格等同於 AND,搜尋不區分大小寫。使用引號可在一個值中包含空格。 + + + + Find a field… + 尋找欄位… + + + + Text can be entered plainly or in quotes. Integer fields support <, <=, > and >=. For date fields, the integer is a number of days (added>7 means added within the last 7 days). + 文字可直接輸入或放在引號中。整數欄位支援 <、<=、> 與 >=。在日期欄位中,整數表示天數(added>7 表示在最近 7 天內加入)。 + + + + Field + 欄位 + + + + Description + 說明 + + + + Input + 輸入 + + + + Example + 範例 + + + + Examples show the pattern—replace the values with your own. + 範例會顯示格式;請將值換成您自己的值。 + + + + Query + 查詢 + + + + What it finds + 尋找內容 + + + + Common filters + 常用篩選條件 + + + + Unread comics + 未讀漫畫 + + + + Comics in progress + 閱讀中的漫畫 + + + + Highly rated comics + 高評分漫畫 + + + + Comics added in the last 7 days + 最近 7 天內加入的漫畫 + + + + Metadata + 中繼資料 + + + + Search by series + 依系列搜尋 + + + + Search by writer + 依編劇搜尋 + + + + Manga comics + 日式漫畫 + + + + Search textual tags + 搜尋文字標籤 + + + + Advanced combinations + 進階組合 + + + + Match either writer + 符合任一編劇 + + + + Group alternatives + 將替代條件分組 + + + + Exclude a value + 排除某個值 + + + + Older, highly rated comics + 較早的高評分漫畫 + + + + Copy query + 複製查詢 + + + + Spaces behave like AND. Use quotes for phrases and parentheses to control grouping. + 空格等同於 AND。使用引號表示片語,使用括號控制分組。 + + SearchVolume @@ -2419,40 +3273,82 @@ To stop an automatic update tap on the loading indicator next to the Libraries t ServerConfigDialog - - set port - 設置端口 + + Set port + set port + 設定連接埠 - Server connectivity information - 伺服器連接資訊 + 伺服器連接資訊 - Scan it! - 掃一掃! + 掃一掃! - YACReader is available for iOS and Android devices.<br/>Discover it for <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> or <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>. - YACReader 適用於 iOS 和 Android 裝置。 <br/>發現它適用於 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 或 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>。 + YACReader 適用於 iOS 和 Android 裝置。 <br/>發現它適用於 <a href='https://ios.yacreader.com' style='color:rgb(193, 148, 65)'>iOS</a> 或 <a href='https://android.yacreader.com' style='color:rgb(193, 148, 65)'>Android</a>。 - Choose an IP address - 選擇IP地址 + 選擇IP地址 + + + + + Server connectivity + 伺服器連線 + + + + Scan to connect + 掃描以連線 + + + + Devices on this network can reach your library at the address below. + 此網路中的裝置可透過以下位址存取您的資料庫。 - + + IP address + IP 位址 + + + Port 端口 - - enable the server + + Web interface + 網頁介面 + + + + Copy link + 複製連結 + + + + Open web UI + 開啟網頁介面 + + + + Enable the server 啟用伺服器 + + + YACReader is available for iOS and Android. Discover it for <a href='https://ios.yacreader.com'>iOS</a> or <a href='https://android.yacreader.com'>Android</a>. + YACReader 支援 iOS 與 Android。取得 <a href='https://ios.yacreader.com'>iOS</a> 或 <a href='https://android.yacreader.com'>Android</a> 版本。 + + + enable the server + 啟用伺服器 + SortVolumeComics @@ -2870,14 +3766,14 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReader::WhatsNewDialog - + Release notes are not available. - + 版本資訊無法使用。 - + Previous versions - + 舊版本 @@ -2913,22 +3809,32 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderOptionsDialog - + Save 保存 - + Cancel 取消 - + + Keyboard shortcuts + 鍵盤快速鍵 + + + + Customize the keyboard shortcuts used by the application. + 自訂應用程式使用的鍵盤快速鍵。 + + + Edit shortcuts 編輯快捷鍵 - + Shortcuts 快捷鍵 @@ -2936,7 +3842,12 @@ To stop an automatic update tap on the loading indicator next to the Libraries t YACReaderSearchLineEdit - + + Search filters + 搜尋篩選條件 + + + type to search 搜索類型 diff --git a/YACReaderLibraryServer/console_ui_library_creator.cpp b/YACReaderLibraryServer/console_ui_library_creator.cpp index e4f6a2517..1671c972b 100644 --- a/YACReaderLibraryServer/console_ui_library_creator.cpp +++ b/YACReaderLibraryServer/console_ui_library_creator.cpp @@ -1,5 +1,6 @@ #include "console_ui_library_creator.h" +#include "comic_info_repairer.h" #include "library_creator.h" #include "xml_info_library_scanner.h" #include "yacreader_libraries.h" @@ -50,12 +51,13 @@ void ConsoleUILibraryCreator::createLibrary(const QString &name, const QString & yacreaderLibraries.save(); } -void ConsoleUILibraryCreator::updateLibrary(const QString &path) +bool ConsoleUILibraryCreator::updateLibrary(const QString &path) { + operationFailed = false; QDir pathDir(path); if (!pathDir.exists()) { std::cout << "Directory not found." << std::endl; - return; + return false; } QEventLoop eventLoop; @@ -74,6 +76,7 @@ void ConsoleUILibraryCreator::updateLibrary(const QString &path) libraryCreator->start(); eventLoop.exec(); + return !operationFailed; } void ConsoleUILibraryCreator::addExistingLibrary(const QString &name, const QString &path) @@ -140,6 +143,82 @@ void ConsoleUILibraryCreator::rescanXMLInfoLibrary(const QString &path) eventLoop.exec(); } +int ConsoleUILibraryCreator::repairLibrary(const QString &path) +{ + QDir pathDir(path); + if (!pathDir.exists()) { + std::cout << "Directory not found." << std::endl; + return 1; + } + + QEventLoop eventLoop; + ComicInfoRepairer *repairer = new ComicInfoRepairer(settings); + const auto cleanPath = QDir::cleanPath(pathDir.absolutePath()); + + connect(repairer, &ComicInfoRepairer::comicProcessed, this, &ConsoleUILibraryCreator::newComic); + connect(repairer, &QThread::finished, &eventLoop, &QEventLoop::quit); + + auto runRepair = [&](bool removeStaleLock) { + std::cout << "Repairing comics"; + repairer->repairLibrary(cleanPath, LibraryPaths::libraryDataPath(cleanPath), removeStaleLock); + eventLoop.exec(); + return repairer->summary(); + }; + + auto summary = runRepair(false); + + if (summary.lockedByAnotherProcess) { + if (summary.lockHolderIsRunningLocally) { + std::cout << std::endl + << "A repair of this library is already running (" << summary.lockHolderInfo.toStdString() << "). Wait for it to finish." << std::endl; + delete repairer; + return 1; + } + + std::cout << std::endl; + if (summary.lockHolderInfo.isEmpty()) { + std::cout << "The library is locked by a repair that did not finish." << std::endl; + } else { + std::cout << "The library is locked by a repair started by " << summary.lockHolderInfo.toStdString() << "." << std::endl; + } + std::cout << "If you are sure that no other repair is running, the lock can be removed." << std::endl + << "Remove the lock and continue? [y/N] " << std::flush; + + std::string answer; + std::getline(std::cin, answer); + // piped input can keep the trailing carriage return on Windows + const auto trimmedAnswer = QString::fromStdString(answer).trimmed().toLower(); + if (trimmedAnswer != "y" && trimmedAnswer != "yes") { + delete repairer; + return 1; + } + + summary = runRepair(true); + if (summary.lockedByAnotherProcess) { + std::cout << std::endl + << "The library is still locked, another process took the lock." << std::endl; + delete repairer; + return 1; + } + } + + if (!summary.error.isEmpty()) { + std::cout << std::endl + << "Repair failed: " << summary.error.toStdString() << std::endl; + delete repairer; + return 1; + } + std::cout << std::endl + << "Repaired: " << summary.repaired << std::endl + << "Failed: " << summary.failed << std::endl + << "Missing files: " << summary.missingFiles << std::endl; + for (const auto &failedPath : summary.failedFilePaths) { + std::cout << " " << failedPath.toStdString() << std::endl; + } + delete repairer; + return 0; +} + void ConsoleUILibraryCreator::newComic(const QString & /*relativeComicPath*/, const QString & /*coverPath*/) { numComicsProcessed++; @@ -154,6 +233,7 @@ void ConsoleUILibraryCreator::manageCreatingError(const QString &error) void ConsoleUILibraryCreator::manageUpdatingError(const QString &error) { + operationFailed = true; std::cout << std::endl << "Error updating library! " << error.toUtf8().constData() << std::endl; } diff --git a/YACReaderLibraryServer/console_ui_library_creator.h b/YACReaderLibraryServer/console_ui_library_creator.h index 6049aafcb..e08f060c6 100644 --- a/YACReaderLibraryServer/console_ui_library_creator.h +++ b/YACReaderLibraryServer/console_ui_library_creator.h @@ -9,14 +9,16 @@ class ConsoleUILibraryCreator : public QObject public: explicit ConsoleUILibraryCreator(QSettings *settings, QObject *parent = 0); void createLibrary(const QString &name, const QString &path); - void updateLibrary(const QString &path); + bool updateLibrary(const QString &path); void addExistingLibrary(const QString &name, const QString &path); void removeLibrary(const QString &name); void rescanXMLInfoLibrary(const QString &path); + int repairLibrary(const QString &path); private: uint numComicsProcessed; QSettings *settings; + bool operationFailed { false }; signals: public slots: diff --git a/YACReaderLibraryServer/libraries_updater.cpp b/YACReaderLibraryServer/libraries_updater.cpp index 1b2631eb8..b89c46b76 100644 --- a/YACReaderLibraryServer/libraries_updater.cpp +++ b/YACReaderLibraryServer/libraries_updater.cpp @@ -18,6 +18,11 @@ void LibrariesUpdater::updateIfNeeded() for (const QString &name : libraries.getNames()) { QString libraryPath = libraries.getPath(name); + QString recoveryError; + if (!DataBaseManagement::recoverInterruptedRestore(libraryPath, &recoveryError)) { + qWarning() << "Unable to recover interrupted restore for" << libraryPath << recoveryError; + continue; + } QString libraryDataPath = YACReader::LibraryPaths::libraryDataPath(libraryPath); QString databasePath = YACReader::LibraryPaths::libraryDatabasePath(libraryPath); diff --git a/YACReaderLibraryServer/main.cpp b/YACReaderLibraryServer/main.cpp index 2d863322a..53055e306 100644 --- a/YACReaderLibraryServer/main.cpp +++ b/YACReaderLibraryServer/main.cpp @@ -31,6 +31,11 @@ int removeLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings * int listLibraries(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout); int setPort(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout); int rescanXmlInfo(QCoreApplication &app, QCommandLineParser &parser, QSettings *settings); +int repairLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings *settings); +int backupLibrary(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout); +int listBackups(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout); +int restoreLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings *settings, QTextStream &qout); +int repairLibraryDb(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout); void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); void printServerInfo(YACReaderHttpServer *httpServer); @@ -81,10 +86,13 @@ int main(int argc, char **argv) .arg(settingsPath)); parser.addHelpOption(); const QCommandLineOption versionOption = parser.addVersionOption(); - parser.addPositionalArgument("command", "The command to execute. [start, create-library, update-library, add-library, remove-library, list-libraries, set-port, rescan-xml-info]"); + parser.addPositionalArgument("command", "The command to execute. [start, create-library, update-library, repair-library, backup-library, list-backups, restore-library, repair-library-db, add-library, remove-library, list-libraries, set-port, rescan-xml-info]"); parser.addOption({ "loglevel", "Set log level. Valid values: trace, info, debug, warn, error.", "loglevel", "info" }); parser.addOption({ "port", "Set server port (temporary). Valid values: 1-65535", "port" }); parser.addOption({ "system-info", "Prints detailed information about the system environment, including OS version, hardware specifications, and available resources." }); + parser.addOption({ "update-library", "Run a full library update after restoring a backup." }); + parser.addOption({ "allow-invalid-current", "Allow restore when the current library database is invalid." }); + parser.addOption({ "remove-stale-lock", "Remove a stale maintenance lock after validating that it is stale." }); parser.parse(app.arguments()); const QStringList args = parser.positionalArguments(); @@ -115,6 +123,16 @@ int main(int argc, char **argv) return createLibrary(app, parser, settings); } else if (command == "update-library") { return updateLibrary(app, parser, settings); + } else if (command == "repair-library") { + return repairLibrary(app, parser, settings); + } else if (command == "backup-library") { + return backupLibrary(app, parser, qout); + } else if (command == "list-backups") { + return listBackups(app, parser, qout); + } else if (command == "restore-library") { + return restoreLibrary(app, parser, settings, qout); + } else if (command == "repair-library-db") { + return repairLibraryDb(app, parser, qout); } else if (command == "add-library") { return addLibrary(app, parser, settings); } else if (command == "remove-library") { @@ -308,11 +326,143 @@ int updateLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings * } ConsoleUILibraryCreator *libraryCreatorUI = new ConsoleUILibraryCreator(settings); - libraryCreatorUI->updateLibrary(args.at(1)); + return libraryCreatorUI->updateLibrary(args.at(1)) ? 0 : 1; +} + +int backupLibrary(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout) +{ + parser.clearPositionalArguments(); + parser.addPositionalArgument("backup-library", "Creates a manual database backup"); + parser.addPositionalArgument("path", "Path to the library", ""); + parser.addPositionalArgument("destination", "Destination .ydb file", ""); + parser.process(app); + + const auto args = parser.positionalArguments(); + if (args.length() != 3) { + parser.showHelp(1); + return 1; + } + + QString error; + if (!DataBaseManagement::backupLibrary(args.at(1), DatabaseBackupReason::Manual, &error, args.at(2))) { + qout << "Backup failed: " << error << Qt::endl; + return 1; + } + qout << "Backup created: " << QDir::cleanPath(args.at(2)) << Qt::endl; + return 0; +} + +int listBackups(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout) +{ + parser.clearPositionalArguments(); + parser.addPositionalArgument("list-backups", "Lists managed database backups newest first"); + parser.addPositionalArgument("path", "Path to the library", ""); + parser.process(app); + + const auto args = parser.positionalArguments(); + if (args.length() != 2) { + parser.showHelp(1); + return 1; + } + + for (const auto &backup : DataBaseManagement::libraryBackups(args.at(1))) + qout << backup.fileName() << '\t' << backup.absoluteFilePath() << Qt::endl; + return 0; +} + +int restoreLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings *settings, QTextStream &qout) +{ + parser.clearPositionalArguments(); + parser.addPositionalArgument("restore-library", "Restores a library database backup"); + parser.addPositionalArgument("path", "Path to the library", ""); + parser.addPositionalArgument("backup", "Backup .ydb file", ""); + parser.process(app); + + const auto args = parser.positionalArguments(); + if (args.length() != 3) { + parser.showHelp(1); + return 1; + } + if (YACReaderLocalServer::isRunning()) { + qout << "Restore failed: stop YACReaderLibraryServer and other YACReader applications first." << Qt::endl; + return 3; + } + + const auto result = DataBaseManagement::restoreLibrary(args.at(1), args.at(2), parser.isSet("allow-invalid-current"), parser.isSet("remove-stale-lock")); + if (!result.success()) { + qout << "Restore failed: " << result.error << Qt::endl; + if (result.status == DatabaseRestoreStatus::LockFailed) + return 3; + if (result.status == DatabaseRestoreStatus::RollbackFailed) + return 4; + return 1; + } + + qout << "Database restored (version " << result.restoredVersion << ")" << Qt::endl; + if (parser.isSet("update-library")) { + ConsoleUILibraryCreator libraryCreatorUI(settings); + if (!libraryCreatorUI.updateLibrary(args.at(1))) { + qout << "Restore succeeded, but the library update failed." << Qt::endl; + return 5; + } + } + return 0; +} + +int repairLibraryDb(QCoreApplication &app, QCommandLineParser &parser, QTextStream &qout) +{ + parser.clearPositionalArguments(); + parser.addPositionalArgument("repair-library-db", "Tries to repair a damaged library database"); + parser.addPositionalArgument("path", "Path to the library", ""); + parser.process(app); + + const auto args = parser.positionalArguments(); + if (args.length() != 2) { + parser.showHelp(1); + return 1; + } + + const auto result = DataBaseManagement::salvageLibrary(args.at(1), parser.isSet("remove-stale-lock")); + if (!result.success()) { + qout << "Repair failed: " << result.error << Qt::endl; + if (!result.preservedDatabasePath.isEmpty()) + qout << "The damaged original was preserved at: " << result.preservedDatabasePath << Qt::endl; + qout << "You can restore a backup with restore-library or recreate the library." << Qt::endl; + return result.status == DatabaseSalvageStatus::LockFailed ? 3 : 1; + } + if (result.status == DatabaseSalvageStatus::AlreadyValid) { + qout << "Library database is already valid." << Qt::endl; + } else if (result.status == DatabaseSalvageStatus::Reindexed) { + qout << "Library database repaired by rebuilding its indexes." << Qt::endl; + } else { + qout << "Library database repaired by rebuilding it." << Qt::endl; + } + if (!result.preservedDatabasePath.isEmpty()) + qout << "The damaged original was preserved at: " << result.preservedDatabasePath << Qt::endl; return 0; } +// ----------------------------------------------------------------------------- +// repair-library--------------------------------------------------------------- +// ----------------------------------------------------------------------------- +int repairLibrary(QCoreApplication &app, QCommandLineParser &parser, QSettings *settings) +{ + parser.clearPositionalArguments(); + parser.addPositionalArgument("repair-library", "Repairs missing covers and incomplete comic information in an existing library at "); + parser.addPositionalArgument("path", "Path to the library to repair", ""); + parser.process(app); + + const QStringList args = parser.positionalArguments(); + if (args.length() != 2) { + parser.showHelp(1); + return 1; + } + + ConsoleUILibraryCreator libraryCreatorUI(settings); + return libraryCreatorUI.repairLibrary(args.at(1)); +} + // ----------------------------------------------------------------------------- // add-library--------------------------------------------------------------- // ----------------------------------------------------------------------------- diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_de.ts b/YACReaderLibraryServer/yacreaderlibraryserver_de.ts index 0e7a5f38b..4091bbf16 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_de.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_de.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly CRC Error auf Seite (%1): Einige Seiten werden nicht korrekt dargestellt - + Unknown error opening the file Unbekannter Fehler beim Öffnen des Files - + 7z not found 7z nicht gefunden - + Format not supported Format wird nicht unterstützt + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_es.ts b/YACReaderLibraryServer/yacreaderlibraryserver_es.ts index b7d79a8cd..c661f56c3 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_es.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_es.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly Error CRC en la página (%1): algunas de las páginas no se mostrarán correctamente - + Unknown error opening the file Error desconocido abriendo el archivo - + 7z not found 7z no encontrado - + Format not supported Formato no soportado + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_fr.ts b/YACReaderLibraryServer/yacreaderlibraryserver_fr.ts index 3700cfe57..e4b7cb86a 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_fr.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_fr.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly Erreur CRC sur la page (%1): certaines pages ne s'afficheront pas correctement - + Unknown error opening the file Erreur inconnue lors de l'ouverture du fichier - + 7z not found 7z introuvable - + Format not supported Format non supporté + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_ko.ts b/YACReaderLibraryServer/yacreaderlibraryserver_ko.ts index b1762cb19..b9e9bc894 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_ko.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_ko.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly %1번 페이지에서 CRC 오류 발생: 일부 페이지가 올바르게 표시되지 않을 수 있습니다 - + Unknown error opening the file 파일을 여는 중 알 수 없는 오류가 발생했습니다 - + 7z not found 7z를 찾을 수 없습니다 - + Format not supported 지원하지 않는 형식입니다 + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_nl.ts b/YACReaderLibraryServer/yacreaderlibraryserver_nl.ts index 187515d95..045113833 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_nl.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_nl.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly CRC-fout op pagina (%1): sommige pagina's worden niet correct weergegeven - + Unknown error opening the file Onbekende fout bij het openen van het bestand - + 7z not found 7Z Archiefbestand niet gevonden - + Format not supported Formaat niet ondersteund + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_pt.ts b/YACReaderLibraryServer/yacreaderlibraryserver_pt.ts index 535b73483..24df47418 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_pt.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_pt.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly Erro CRC na página (%1): algumas páginas não serão exibidas corretamente - + Unknown error opening the file Erro desconhecido ao abrir o arquivo - + 7z not found 7z não encontrado - + Format not supported Formato não suportado + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_ru.ts b/YACReaderLibraryServer/yacreaderlibraryserver_ru.ts index 46dcab0eb..81d6a2beb 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_ru.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_ru.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly Ошибка контрольной суммы CRC на странице (%1): некоторые страницы будут отображаться неправильно - + Unknown error opening the file Неизвестная ошибка при открытии файла - + 7z not found 7z не найден - + Format not supported Формат не поддерживается + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_source.ts b/YACReaderLibraryServer/yacreaderlibraryserver_source.ts index 44f6926be..7e16d1045 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_source.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_source.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly - + Unknown error opening the file - + 7z not found - + Format not supported + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_tr.ts b/YACReaderLibraryServer/yacreaderlibraryserver_tr.ts index cdc76576d..1f82466cd 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_tr.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_tr.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly (%1). sayfada CRC hatası : bazı sayfalar düzgün görüntülenmeyecek - + Unknown error opening the file Dosya açılırken bilinmeyen hata - + 7z not found 7z bulunamadı - + Format not supported Biçim desteklenmiyor + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_zh_CN.ts b/YACReaderLibraryServer/yacreaderlibraryserver_zh_CN.ts index defaf2756..526cf7753 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_zh_CN.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_zh_CN.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 页 CRC 校验失败: 部分页面将无法正确显示 - + Unknown error opening the file 打开文件时出现未知错误 - + 7z not found 未找到 7z - + Format not supported 不支持的文件格式 + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_zh_HK.ts b/YACReaderLibraryServer/yacreaderlibraryserver_zh_HK.ts index c30159f3c..2cd1d3d14 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_zh_HK.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_zh_HK.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/YACReaderLibraryServer/yacreaderlibraryserver_zh_TW.ts b/YACReaderLibraryServer/yacreaderlibraryserver_zh_TW.ts index fb54797c1..873dee960 100644 --- a/YACReaderLibraryServer/yacreaderlibraryserver_zh_TW.ts +++ b/YACReaderLibraryServer/yacreaderlibraryserver_zh_TW.ts @@ -4,30 +4,35 @@ FileComic - + CRC error on page (%1): some of the pages will not be displayed correctly 第 %1 頁 CRC 校驗失敗: 部分頁面將無法正確顯示 - + Unknown error opening the file 打開檔時出現未知錯誤 - + 7z not found 未找到 7z - + Format not supported 不支持的檔格式 + + + Unsupported EPUB: %1 + + QCoreApplication - + YACReaderLibraryServer is the headless (no gui) version of YACReaderLibrary. diff --git a/ci/win/build_installer_qt6.iss b/ci/win/build_installer_qt6.iss index 9cc161bb6..5bfc8dc8f 100644 --- a/ci/win/build_installer_qt6.iss +++ b/ci/win/build_installer_qt6.iss @@ -29,8 +29,8 @@ Root: HKCR; Subkey: Comic Book (rar)\DefaultIcon; ValueType: string; ValueData: Root: HKCR; Subkey: .clc; ValueType: string; ValueData: Compressed Library Covers (clc); Flags: uninsdeletekey Root: HKCR; SubKey: Compressed Library Covers (clc); ValueType: string; ValueData: Compressed Library Covers; Flags: uninsdeletekey Root: HKCR; Subkey: Compressed Library Covers (clc)\DefaultIcon; ValueType: string; ValueData: {app}\YACReaderLibrary.exe,1; Flags: uninsdeletevalue -Root: HKCR; Subkey: .ydb; ValueType: string; ValueData: Compressed Library Covers (clc); Flags: uninsdeletekey -Root: HKCR; SubKey: YACReader Data Base (ydb); ValueType: string; ValueData: Compressed Library Covers; Flags: uninsdeletekey +Root: HKCR; Subkey: .ydb; ValueType: string; ValueData: YACReader Data Base (ydb); Flags: uninsdeletekey +Root: HKCR; SubKey: YACReader Data Base (ydb); ValueType: string; ValueData: YACReader Library Database; Flags: uninsdeletekey Root: HKCR; Subkey: YACReader Data Base (ydb)\DefaultIcon; ValueType: string; ValueData: {app}\YACReaderLibrary.exe,1; Flags: uninsdeletevalue [Files] diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 300ee6b65..822540bfa 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -34,6 +34,15 @@ target_include_directories(naturalsort PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) yacreader_apply_build_options(naturalsort) target_link_libraries(naturalsort PUBLIC Qt6::Core) +# --- EPUB page index (metadata-only; no rendering) --- +add_library(epub_page_index STATIC + epub_page_index.h + epub_page_index.cpp +) +target_include_directories(epub_page_index PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +yacreader_apply_build_options(epub_page_index) +target_link_libraries(epub_page_index PUBLIC Qt6::Core) + # --- concurrent_queue --- add_library(concurrent_queue STATIC concurrent_queue.h @@ -104,6 +113,7 @@ target_link_libraries(comic_backend PUBLIC common_all pdf_backend_iface cbx_backend + epub_page_index QsLog ) diff --git a/common/comic.cpp b/common/comic.cpp index 398b024b3..2cdffc0fd 100644 --- a/common/comic.cpp +++ b/common/comic.cpp @@ -4,6 +4,7 @@ #include "bookmarks.h" //TODO desacoplar la dependencia con bookmarks #include "comic_db.h" #include "compressed_archive.h" +#include "epub_page_index.h" #include "pdf_render_size.h" #include "qnaturalsorting.h" @@ -18,6 +19,10 @@ #include #include +#ifdef use_unarr +#include +#endif + enum YACReaderPageSortingMode { YACReaderNumericalSorting, YACReaderHeuristicSorting, @@ -66,6 +71,7 @@ const QStringList Comic::literalImageExtensions = QStringList() << "jpg" #ifndef use_unarr const QStringList ComicArchiveExtensions = QStringList() << "*.cbr" << "*.cbz" + << "*.epub" << "*.rar" << "*.zip" << "*.tar" @@ -75,6 +81,7 @@ const QStringList ComicArchiveExtensions = QStringList() << "*.cbr" << "*.cbt"; const QStringList LiteralComicArchiveExtensions = QStringList() << "cbr" << "cbz" + << "epub" << "rar" << "zip" << "tar" @@ -85,19 +92,25 @@ const QStringList LiteralComicArchiveExtensions = QStringList() << "cbr" #else const QStringList ComicArchiveExtensions = QStringList() << "*.cbr" << "*.cbz" + << "*.epub" << "*.rar" << "*.zip" << "*.tar" +#if (UNARR_API_VERSION >= 110) << "*.7z" << "*.cb7" +#endif << "*.cbt"; const QStringList LiteralComicArchiveExtensions = QStringList() << "cbr" << "cbz" + << "epub" << "rar" << "zip" << "tar" +#if (UNARR_API_VERSION >= 110) << "7z" << "cb7" +#endif << "cbt"; #endif // use_unarr #ifndef NO_PDF @@ -298,7 +311,12 @@ bool Comic::hasBeenAnErrorOpening() bool Comic::fileIsComic(const QString &path) { QFileInfo info(path); - return literalComicExtensions.contains(info.suffix()); + return literalComicExtensions.contains(info.suffix(), Qt::CaseInsensitive); +} + +bool Comic::fileIsEpub(const QString &path) +{ + return QFileInfo(path).suffix().compare(QStringLiteral("epub"), Qt::CaseInsensitive) == 0; } QList Comic::findValidComicFiles(const QList &list) @@ -362,9 +380,8 @@ FileComic::~FileComic() { _pages.clear(); _loadedPages.clear(); - _fileNames.clear(); - _newOrder.clear(); - _order.clear(); + _pageArchiveIndexes.clear(); + _archiveIndexToPages.clear(); } bool FileComic::load(const QString &path, int atPage) @@ -413,21 +430,11 @@ bool FileComic::load(const QString &path, const ComicDB &comic) QList FileComic::filter(const QList &src) { - QList extensions = getSupportedImageLiteralFormats(); + const QStringList extensions = getSupportedImageLiteralFormats(); QList filtered; - bool fileAccepted = false; for (const QString &fileName : std::as_const(src)) { - fileAccepted = false; - if (!fileName.contains("__MACOSX")) { - for (const QString &extension : std::as_const(extensions)) { - if (fileName.endsWith(extension, Qt::CaseInsensitive)) { - fileAccepted = true; - break; - } - } - } - if (fileAccepted) { + if (isSupportedImage(fileName, extensions)) { filtered.append(fileName); } } @@ -435,26 +442,75 @@ QList FileComic::filter(const QList &src) return filtered; } +bool FileComic::isSupportedImage(const QString &fileName, const QStringList &supportedExtensions) +{ + if (fileName.contains("__MACOSX")) { + return false; + } + for (const QString &extension : supportedExtensions) { + if (fileName.endsWith(extension, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +YACReaderEpub::PageIndex FileComic::epubPageIndex(const QStringList &fileNames, CompressedArchive &archive) +{ + auto result = YACReaderEpub::readPageIndex(fileNames, [&archive](int index) { return archive.getRawDataAtIndex(index); }); + if (!result.isValid()) { + return result; + } + + QStringList pageNames; + for (const YACReaderEpub::Page &page : std::as_const(result.pages)) { + pageNames.append(page.fileName); + } + QSet supportedPageNames; + for (const QString &pageName : filter(pageNames)) { + supportedPageNames.insert(pageName); + } + + QVector supportedPages; + for (const YACReaderEpub::Page &page : std::as_const(result.pages)) { + if (supportedPageNames.contains(page.fileName)) { + supportedPages.append(page); + } + } + result.pages = std::move(supportedPages); + if (result.pages.isEmpty()) { + result.error = QStringLiteral("Package spine contains no supported image pages"); + } + return result; +} + +YACReaderEpub::ScanInfo FileComic::epubScanInfo(const QStringList &fileNames, CompressedArchive &archive, int coverPage) +{ + const QStringList supportedExtensions = Comic::getSupportedImageLiteralFormats(); + return YACReaderEpub::readScanInfo(fileNames, [&archive](int index) { return archive.getRawDataAtIndex(index); }, coverPage, [&supportedExtensions](const QString &fileName) { return isSupportedImage(fileName, supportedExtensions); }); +} + // DELEGATE methods void FileComic::fileExtracted(int index, const QByteArray &rawData) { - /*QFile f("c:/temp/out2.txt"); - f.open(QIODevice::Append); - QTextStream out(&f);*/ - int sortedIndex = _fileNames.indexOf(_order.at(index)); - // out << sortedIndex << " , "; - // f.close(); - if (sortedIndex == -1) { - return; + const QVector pages = _archiveIndexToPages.value(index); + for (int page : pages) { + _pages[page] = rawData; + emit imageLoaded(page); + emit imageLoaded(page, _pages[page]); } - _pages[sortedIndex] = rawData; - emit imageLoaded(sortedIndex); - emit imageLoaded(sortedIndex, _pages[sortedIndex]); } void FileComic::crcError(int index) { - emit crcErrorFound(tr("CRC error on page (%1): some of the pages will not be displayed correctly").arg(index + 1)); + const QVector pages = _archiveIndexToPages.value(index); + QStringList pageNumbers; + for (int page : pages) { + pageNumbers.append(QString::number(page + 1)); + } + + const QString affectedPages = pageNumbers.isEmpty() ? QString::number(index + 1) : pageNumbers.join(QStringLiteral(", ")); + emit crcErrorFound(tr("CRC error on page (%1): some of the pages will not be displayed correctly").arg(affectedPages)); } // TODO: comprobar que si se produce uno de estos errores, la carga del c�mic es irrecuperable @@ -474,10 +530,16 @@ bool FileComic::isCancelled() QList> FileComic::getSections(int §ionIndex) { - QVector sortedIndexes; - for (const QString &name : std::as_const(_fileNames)) { - sortedIndexes.append(_order.indexOf(name)); + QVector archiveIndexes; + QSet seenIndexes; + for (quint32 archiveIndex : std::as_const(_pageArchiveIndexes)) { + if (!seenIndexes.contains(archiveIndex)) { + archiveIndexes.append(archiveIndex); + seenIndexes.insert(archiveIndex); + } } + const int firstArchiveIndex = archiveIndexes.indexOf(_pageArchiveIndexes.at(_firstPage)); + QList> sections; quint32 previous = 0; sectionIndex = -1; @@ -485,9 +547,9 @@ QList> FileComic::getSections(int §ionIndex) QVector section; int idx = 0; unsigned int realIdx; - for (const quint32 i : std::as_const(sortedIndexes)) { + for (const quint32 i : std::as_const(archiveIndexes)) { - if (_firstPage == idx) { + if (firstArchiveIndex == idx) { sectionIndex = sectionCount; realIdx = i; } @@ -573,30 +635,48 @@ void FileComic::process() } // se filtran para obtener s�lo los formatos soportados - _order = archive.getFileNames(); - _fileNames = filter(_order); + const QStringList archiveFileNames = archive.getFileNames(); + QStringList pageFileNames; + _pageArchiveIndexes.clear(); + _archiveIndexToPages.clear(); + + if (Comic::fileIsEpub(_path)) { + const auto epub = epubPageIndex(archiveFileNames, archive); + if (!epub.isValid()) { + moveToThread(QCoreApplication::instance()->thread()); + emit errorOpening(tr("Unsupported EPUB: %1").arg(epub.error)); + return; + } - if (_fileNames.size() == 0) { + for (const YACReaderEpub::Page &page : epub.pages) { + pageFileNames.append(page.fileName); + _pageArchiveIndexes.append(page.archiveIndex); + } + } else { + pageFileNames = filter(archiveFileNames); + comic_pages_sort(pageFileNames, YACReaderHeuristicSorting); + for (const QString &fileName : std::as_const(pageFileNames)) { + _pageArchiveIndexes.append(archiveFileNames.indexOf(fileName)); + } + } + + if (pageFileNames.isEmpty()) { // QMessageBox::critical(NULL,tr("File error"),tr("File not found or not images in file")); moveToThread(QCoreApplication::instance()->thread()); emit errorOpening(); return; } - // TODO, cambiar por listas - //_order = _fileNames; - - _pages.resize(_fileNames.size()); - _loadedPages = QVector(_fileNames.size(), false); + _pages.resize(pageFileNames.size()); + _loadedPages = QVector(pageFileNames.size(), false); emit pageChanged(0); // this indicates new comic, index=0 emit numPages(_pages.size()); _loaded = true; - _cfi = 0; - - // TODO, add a setting for choosing the type of page sorting used. - comic_pages_sort(_fileNames, YACReaderHeuristicSorting); + for (int page = 0; page < _pageArchiveIndexes.size(); ++page) { + _archiveIndexToPages[_pageArchiveIndexes.at(page)].append(page); + } if (_firstPage == -1) { _firstPage = bm->getLastPage(); @@ -626,16 +706,6 @@ void FileComic::process() } archive.getAllData(sections.at(i), this); } - // archive.getAllData(QVector(),this); - /* - for (const auto &name : _fileNames) - { - index = _order.indexOf(name); - sortedIndex = _fileNames.indexOf(name); - _pages[sortedIndex] = allData.at(index); - emit imageLoaded(sortedIndex); - emit imageLoaded(sortedIndex,_pages[sortedIndex]); - }*/ moveToThread(QCoreApplication::instance()->thread()); emit imagesLoaded(); } diff --git a/common/comic.h b/common/comic.h index e5fdd2a07..cf1fc66a3 100644 --- a/common/comic.h +++ b/common/comic.h @@ -7,10 +7,17 @@ #include #include #include + #ifndef NO_PDF #include "pdf_comic.h" #endif // NO_PDF class ComicDB; +class CompressedArchive; + +namespace YACReaderEpub { +struct PageIndex; +struct ScanInfo; +} // #define EXTENSIONS_LITERAL << ".jpg" << ".jpeg" << ".png" << ".gif" << ".tiff" << ".tif" << ".bmp" //Comic::getSupportedImageLiteralFormats() class Comic : public QObject @@ -22,15 +29,10 @@ class Comic : public QObject QVector _pages; QVector _loadedPages; // QVector _sizes; - QStringList _fileNames; - QMap _newOrder; - QList _order; int _index; QString _path; bool _loaded; - int _cfi; - // open the comic at this point int _firstPage; @@ -79,6 +81,7 @@ class Comic : public QObject static QStringList getSupportedImageLiteralFormats(); static bool fileIsComic(const QString &path); + static bool fileIsEpub(const QString &path); static QList findValidComicFiles(const QList &list); static QList findValidComicFilesInFolder(const QString &path); @@ -117,6 +120,8 @@ class FileComic : public Comic, public ExtractDelegate private: QList> getSections(int §ionIndex); + QVector _pageArchiveIndexes; + QHash> _archiveIndexToPages; public: FileComic(); @@ -125,6 +130,9 @@ class FileComic : public Comic, public ExtractDelegate bool load(const QString &path, int atPage = -1); bool load(const QString &path, const ComicDB &comic); static QList filter(const QList &src); + static bool isSupportedImage(const QString &fileName, const QStringList &supportedExtensions); + static YACReaderEpub::PageIndex epubPageIndex(const QStringList &fileNames, CompressedArchive &archive); + static YACReaderEpub::ScanInfo epubScanInfo(const QStringList &fileNames, CompressedArchive &archive, int coverPage); // ExtractDelegate void fileExtracted(int index, const QByteArray &rawData); diff --git a/common/epub_page_index.cpp b/common/epub_page_index.cpp new file mode 100644 index 000000000..746eb27c7 --- /dev/null +++ b/common/epub_page_index.cpp @@ -0,0 +1,412 @@ +#include "epub_page_index.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +class HtmlEntityResolver : public QXmlStreamEntityResolver +{ +public: + QString resolveUndeclaredEntity(const QString &) override + { + return QStringLiteral(" "); + } +}; + +struct ManifestItem { + QString href; + QString mediaType; + QString properties; +}; + +struct SpineItem { + QString id; +}; + +struct Package { + QHash manifest; + QVector spine; + bool fixedLayout = false; + QString coverId; +}; + +QString normalizedArchiveName(QString name) +{ + name.replace('\\', '/'); + while (name.startsWith('/')) { + name.remove(0, 1); + } + return QDir::cleanPath(name); +} + +std::optional resolvePath(const QString &documentPath, const QString &reference) +{ + const QUrl url = QUrl::fromEncoded(reference.toUtf8()); + if (!url.isRelative() || url.path().isEmpty()) { + return std::nullopt; + } + + QString path = url.path(QUrl::FullyDecoded); + const bool fromArchiveRoot = path.startsWith('/'); + while (path.startsWith('/')) { + path.remove(0, 1); + } + + const QString baseDirectory = fromArchiveRoot ? QString() : QFileInfo(documentPath).path(); + const QString resolved = normalizedArchiveName(QDir(baseDirectory).filePath(path)); + if (resolved == QStringLiteral("..") || resolved.startsWith(QStringLiteral("../"))) { + return std::nullopt; + } + return resolved; +} + +QString attribute(const QXmlStreamAttributes &attributes, QStringView name) +{ + // Match local names so this handles both unqualified href and xlink:href. + for (const QXmlStreamAttribute &value : attributes) { + if (value.name() == name) { + return value.value().toString(); + } + } + return { }; +} + +bool containsProperty(const QString &properties, QStringView property) +{ + const QStringList values = properties.split(' ', Qt::SkipEmptyParts); + const QString propertyName = property.toString(); + for (const QString &value : values) { + if (value == propertyName || value.endsWith(QStringLiteral(":") + propertyName)) { + return true; + } + } + return false; +} + +std::optional packagePath(const QByteArray &containerXml, QString &error) +{ + QXmlStreamReader reader(containerXml); + QString firstRootFile; + QString preferredRootFile; + + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isStartElement() && reader.name() == QStringLiteral("rootfile")) { + const QString path = attribute(reader.attributes(), u"full-path"); + const QString mediaType = attribute(reader.attributes(), u"media-type"); + if (firstRootFile.isEmpty()) { + firstRootFile = path; + } + if (preferredRootFile.isEmpty() && mediaType == QStringLiteral("application/oebps-package+xml")) { + preferredRootFile = path; + } + } + } + + if (reader.hasError()) { + error = QStringLiteral("Invalid META-INF/container.xml: %1").arg(reader.errorString()); + return std::nullopt; + } + if (firstRootFile.isEmpty()) { + error = QStringLiteral("META-INF/container.xml does not name a package document"); + return std::nullopt; + } + const QString rootFile = preferredRootFile.isEmpty() ? firstRootFile : preferredRootFile; + const auto resolved = resolvePath(QStringLiteral("package.opf"), rootFile); + if (!resolved) { + error = QStringLiteral("Invalid package document path: %1").arg(rootFile); + } + return resolved; +} + +std::optional readPackage(const QByteArray &packageXml, QString &error) +{ + QXmlStreamReader reader(packageXml); + Package package; + bool inManifest = false; + bool inSpine = false; + + while (!reader.atEnd()) { + reader.readNext(); + if (reader.isEndElement()) { + inManifest = inManifest && reader.name() != QStringLiteral("manifest"); + inSpine = inSpine && reader.name() != QStringLiteral("spine"); + continue; + } + if (!reader.isStartElement()) { + continue; + } + + if (reader.name() == QStringLiteral("manifest")) { + inManifest = true; + } else if (reader.name() == QStringLiteral("spine")) { + inSpine = true; + } else if (inManifest && reader.name() == QStringLiteral("item")) { + const QString id = attribute(reader.attributes(), u"id"); + package.manifest.insert(id, { attribute(reader.attributes(), u"href"), attribute(reader.attributes(), u"media-type"), attribute(reader.attributes(), u"properties") }); + } else if (inSpine && reader.name() == QStringLiteral("itemref")) { + if (attribute(reader.attributes(), u"linear") != QStringLiteral("no")) { + package.spine.append({ attribute(reader.attributes(), u"idref") }); + } + } else if (reader.name() == QStringLiteral("meta")) { + const QString property = attribute(reader.attributes(), u"property"); + const QString name = attribute(reader.attributes(), u"name"); + if (property == QStringLiteral("rendition:layout")) { + package.fixedLayout = reader.readElementText().trimmed() == QStringLiteral("pre-paginated"); + } else if (name == QStringLiteral("fixed-layout")) { + package.fixedLayout = attribute(reader.attributes(), u"content") == QStringLiteral("true"); + } else if (name == QStringLiteral("cover")) { + package.coverId = attribute(reader.attributes(), u"content"); + } + } + } + + if (reader.hasError()) { + error = QStringLiteral("Invalid package document: %1").arg(reader.errorString()); + return std::nullopt; + } + if (package.manifest.isEmpty() || package.spine.isEmpty()) { + error = QStringLiteral("Package document has no manifest or linear spine items"); + return std::nullopt; + } + return package; +} + +std::optional imageFromWrapper(const QByteArray &document, const QString &documentPath, const QHash &archiveIndexes) +{ + HtmlEntityResolver entityResolver; + QXmlStreamReader reader(document); + reader.setEntityResolver(&entityResolver); + QSet images; + + while (!reader.atEnd()) { + reader.readNext(); + if (!reader.isStartElement()) { + continue; + } + + QString reference; + if (reader.name() == QStringLiteral("img")) { + reference = attribute(reader.attributes(), u"src"); + } else if (reader.name() == QStringLiteral("image")) { + reference = attribute(reader.attributes(), u"href"); + } else if (reader.name() == QStringLiteral("object")) { + reference = attribute(reader.attributes(), u"data"); + } + if (reference.isEmpty()) { + continue; + } + + const auto resolved = resolvePath(documentPath, reference); + if (resolved && archiveIndexes.contains(*resolved)) { + images.insert(*resolved); + } + } + + if (reader.hasError()) { + return std::nullopt; + } + if (images.size() != 1) { + return std::nullopt; + } + return *images.constBegin(); +} + +struct Book { + QHash archiveIndexes; + QString packagePath; + Package package; +}; + +std::optional readBook(const QStringList &fileNames, const YACReaderEpub::FileReader &readFile, QString &error) +{ + Book book; + for (int index = 0; index < fileNames.size(); ++index) { + book.archiveIndexes.insert(normalizedArchiveName(fileNames.at(index)), index); + } + + const int containerIndex = book.archiveIndexes.value(QStringLiteral("META-INF/container.xml"), -1); + if (containerIndex < 0) { + error = QStringLiteral("Missing META-INF/container.xml"); + return std::nullopt; + } + + const auto opfPath = packagePath(readFile(containerIndex), error); + if (!opfPath) { + return std::nullopt; + } + const int opfIndex = book.archiveIndexes.value(*opfPath, -1); + if (opfIndex < 0) { + error = QStringLiteral("Package document not found: %1").arg(*opfPath); + return std::nullopt; + } + + const auto package = readPackage(readFile(opfIndex), error); + if (!package) { + return std::nullopt; + } + book.packagePath = *opfPath; + book.package = *package; + return book; +} + +std::optional packageCoverPath(const Book &book) +{ + for (auto item = book.package.manifest.cbegin(); item != book.package.manifest.cend(); ++item) { + if (containsProperty(item->properties, u"cover-image") || item.key() == book.package.coverId) { + return resolvePath(book.packagePath, item->href); + } + } + return std::nullopt; +} + +YACReaderEpub::PageIndex pageIndexFromBook(const Book &book, const YACReaderEpub::FileReader &readFile, const YACReaderEpub::ImageFilter &acceptImage = { }) +{ + YACReaderEpub::PageIndex result; + result.fixedLayout = book.package.fixedLayout; + if (const auto coverPath = packageCoverPath(book)) { + result.coverPath = *coverPath; + } + + for (const SpineItem &spineItem : book.package.spine) { + const auto manifestItem = book.package.manifest.constFind(spineItem.id); + if (manifestItem == book.package.manifest.cend()) { + continue; + } + + const auto contentPath = resolvePath(book.packagePath, manifestItem->href); + if (!contentPath) { + continue; + } + + QString imagePath; + if (manifestItem->mediaType.startsWith(QStringLiteral("image/")) && manifestItem->mediaType != QStringLiteral("image/svg+xml")) { + imagePath = *contentPath; + } else if (manifestItem->mediaType == QStringLiteral("application/xhtml+xml") || manifestItem->mediaType == QStringLiteral("image/svg+xml")) { + const int wrapperIndex = book.archiveIndexes.value(*contentPath, -1); + if (wrapperIndex < 0) { + continue; + } + const auto wrapperImage = imageFromWrapper(readFile(wrapperIndex), *contentPath, book.archiveIndexes); + if (!wrapperImage) { + continue; + } + imagePath = *wrapperImage; + } else { + continue; + } + + const int imageIndex = book.archiveIndexes.value(imagePath, -1); + if (imageIndex < 0 || (acceptImage && !acceptImage(imagePath))) { + continue; + } + result.pages.append({ imagePath, imageIndex }); + } + + if (result.pages.isEmpty()) { + result.error = QStringLiteral("Package spine contains no usable image pages"); + } + return result; +} + +} + +namespace YACReaderEpub { + +PageIndex readPageIndex(const QStringList &fileNames, const FileReader &readFile) +{ + QString error; + const auto book = readBook(fileNames, readFile, error); + if (!book) { + PageIndex result; + result.error = error; + return result; + } + return pageIndexFromBook(*book, readFile); +} + +ScanInfo readScanInfo(const QStringList &fileNames, const FileReader &readFile, int coverPage, const ImageFilter &acceptImage) +{ + ScanInfo result; + const auto book = readBook(fileNames, readFile, result.error); + if (!book) { + return result; + } + + const auto scanAllPages = [&] { + ScanInfo fullResult; + const PageIndex pages = pageIndexFromBook(*book, readFile, acceptImage); + fullResult.error = pages.error; + fullResult.pageCount = static_cast(pages.pages.size()); + if (fullResult.pageCount > 0) { + const int coverIndex = coverPage > 0 && coverPage <= fullResult.pageCount ? coverPage - 1 : 0; + fullResult.coverArchiveIndex = pages.pages.at(coverIndex).archiveIndex; + } + return fullResult; + }; + + if (!book->package.fixedLayout) { + return scanAllPages(); + } + + struct Candidate { + QString path; + bool wrapper = false; + }; + QVector candidates; + for (const SpineItem &spineItem : book->package.spine) { + const auto manifestItem = book->package.manifest.constFind(spineItem.id); + if (manifestItem == book->package.manifest.cend()) { + continue; + } + const auto contentPath = resolvePath(book->packagePath, manifestItem->href); + if (!contentPath || !book->archiveIndexes.contains(*contentPath)) { + continue; + } + + if (manifestItem->mediaType.startsWith(QStringLiteral("image/")) && manifestItem->mediaType != QStringLiteral("image/svg+xml")) { + if (!acceptImage || acceptImage(*contentPath)) { + candidates.append({ *contentPath, false }); + } + } else if (manifestItem->mediaType == QStringLiteral("application/xhtml+xml") || manifestItem->mediaType == QStringLiteral("image/svg+xml")) { + candidates.append({ *contentPath, true }); + } + } + + result.pageCount = static_cast(candidates.size()); + if (result.pageCount == 0) { + result.error = QStringLiteral("Package spine contains no usable image pages"); + return result; + } + + if (coverPage <= 1) { + const auto coverPath = packageCoverPath(*book); + if (coverPath && book->archiveIndexes.contains(*coverPath) && (!acceptImage || acceptImage(*coverPath))) { + result.coverArchiveIndex = book->archiveIndexes.value(*coverPath); + return result; + } + } + + const int coverIndex = coverPage > 0 && coverPage <= result.pageCount ? coverPage - 1 : 0; + const Candidate &cover = candidates.at(coverIndex); + QString imagePath = cover.path; + if (cover.wrapper) { + const int wrapperIndex = book->archiveIndexes.value(cover.path); + const auto wrapperImage = imageFromWrapper(readFile(wrapperIndex), cover.path, book->archiveIndexes); + if (!wrapperImage || (acceptImage && !acceptImage(*wrapperImage))) { + return scanAllPages(); + } + imagePath = *wrapperImage; + } + result.coverArchiveIndex = book->archiveIndexes.value(imagePath, -1); + return result; +} + +} diff --git a/common/epub_page_index.h b/common/epub_page_index.h new file mode 100644 index 000000000..96f7ca92f --- /dev/null +++ b/common/epub_page_index.h @@ -0,0 +1,42 @@ +#ifndef EPUB_PAGE_INDEX_H +#define EPUB_PAGE_INDEX_H + +#include +#include +#include +#include + +#include +namespace YACReaderEpub { + +struct Page { + QString fileName; + int archiveIndex = -1; +}; + +struct PageIndex { + QVector pages; + bool fixedLayout = false; + QString coverPath; + QString error; + + bool isValid() const { return error.isEmpty() && !pages.isEmpty(); } +}; + +struct ScanInfo { + int pageCount = 0; + int coverArchiveIndex = -1; + QString error; + + bool isValid() const { return error.isEmpty() && pageCount > 0; } +}; + +using FileReader = std::function; +using ImageFilter = std::function; + +PageIndex readPageIndex(const QStringList &fileNames, const FileReader &readFile); +ScanInfo readScanInfo(const QStringList &fileNames, const FileReader &readFile, int coverPage, const ImageFilter &acceptImage); + +} + +#endif // EPUB_PAGE_INDEX_H diff --git a/common/qnaturalsorting.cpp b/common/qnaturalsorting.cpp index dcc644455..9e5246047 100644 --- a/common/qnaturalsorting.cpp +++ b/common/qnaturalsorting.cpp @@ -33,6 +33,18 @@ bool naturalSortLessThanCI(const QString &left, const QString &right) return (naturalCompare(left, right, Qt::CaseInsensitive) < 0); } +bool comicNumberLessThan(const QVariant &leftNumber, const QString &leftName, + const QVariant &rightNumber, const QString &rightName) +{ + if (leftNumber.isNull() && rightNumber.isNull()) + return naturalSortLessThanCI(leftName, rightName); + + if (!leftNumber.isNull() && !rightNumber.isNull()) + return naturalSortLessThanCI(leftNumber.toString(), rightNumber.toString()); + + return rightNumber.isNull(); +} + bool naturalSortLessThanCIFileInfo(const QFileInfo &left, const QFileInfo &right) { return naturalSortLessThanCI(left.fileName(), right.fileName()); diff --git a/common/qnaturalsorting.h b/common/qnaturalsorting.h index ac3f217c7..02c4a9df0 100644 --- a/common/qnaturalsorting.h +++ b/common/qnaturalsorting.h @@ -7,6 +7,7 @@ #include #include +#include int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity caseSensitivity); bool naturalSortLessThanCS(const QString &left, const QString &right); @@ -14,9 +15,19 @@ bool naturalSortLessThanCI(const QString &left, const QString &right); bool naturalSortLessThanCIFileInfo(const QFileInfo &left, const QFileInfo &right); bool naturalSortLessThanCILibraryItem(LibraryItem *left, LibraryItem *right); -/* TODO, update to use the issue number once the iOS client supports it - * see DBHelper::getFolderComicsFromLibraryForReading - * NOTE, use this only in the server side for now, this way of sorting just matchs what's used in the iOS client +/* The order comics are read in. Issue number wins when both comics have one, + * numbered comics come before unnumbered ones, and file name breaks the tie + * otherwise. Every place that lists the comics of a folder must use this, so + * that what YACReaderLibrary shows and what YACReader walks with next/previous + * are the same sequence. + **/ +bool comicNumberLessThan(const QVariant &leftNumber, const QString &leftName, + const QVariant &rightNumber, const QString &rightName); + +/* Name-only ordering for server responses that mix folders and comics in a single + * list, where there is no issue number to lean on. Reading order is not this: for + * the comics of a folder use DBHelper::getFolderComicsFromLibraryForReading, which + * sorts by issue number the way the clients do. **/ struct LibraryItemSorter { bool operator()(const LibraryItem *a, const LibraryItem *b) const diff --git a/common/themes/appearance_tab_widget.cpp b/common/themes/appearance_tab_widget.cpp index 387ac0944..4e79f9931 100644 --- a/common/themes/appearance_tab_widget.cpp +++ b/common/themes/appearance_tab_widget.cpp @@ -211,11 +211,16 @@ AppearanceTabWidget::AppearanceTabWidget( themeEditor->activateWindow(); }); - auto *layout = new QVBoxLayout(this); - layout->addWidget(modeBox); - layout->addWidget(themeSelBox); - layout->addWidget(themeEditorBox); - layout->addStretch(); + sectionsLayout = new QVBoxLayout(this); + sectionsLayout->addWidget(modeBox); + sectionsLayout->addWidget(themeSelBox); + sectionsLayout->addWidget(themeEditorBox); + sectionsLayout->addStretch(); +} + +void AppearanceTabWidget::addSection(QWidget *section) +{ + sectionsLayout->insertWidget(sectionsLayout->count() - 1, section); } void AppearanceTabWidget::populateCombo(QComboBox *combo, diff --git a/common/themes/appearance_tab_widget.h b/common/themes/appearance_tab_widget.h index 720827ce2..ac0ea50d6 100644 --- a/common/themes/appearance_tab_widget.h +++ b/common/themes/appearance_tab_widget.h @@ -13,6 +13,7 @@ class AppearanceConfiguration; class QComboBox; class QPushButton; +class QVBoxLayout; class ThemeEditorDialog; class ThemeRepository; @@ -27,6 +28,8 @@ class AppearanceTabWidget : public QWidget std::function applyTheme, QWidget *parent = nullptr); + void addSection(QWidget *section); + private: AppearanceConfiguration *config; ThemeRepository *repository; @@ -47,6 +50,8 @@ class AppearanceTabWidget : public QWidget QPushButton *darkDeleteBtn = nullptr; QPushButton *customDeleteBtn = nullptr; + QVBoxLayout *sectionsLayout; + // Populate a combo with themes, filtered strictly by variant (or all if nullopt). void populateCombo(QComboBox *combo, std::optional variantFilter, const QString &selectedId); void repopulateCombos(); diff --git a/common/yacreader_global_gui.h b/common/yacreader_global_gui.h index 03f580fd5..d671b30ea 100644 --- a/common/yacreader_global_gui.h +++ b/common/yacreader_global_gui.h @@ -12,6 +12,9 @@ #define UI_LANGUAGE "UI_LANGUAGE" #define MAG_GLASS_SIZE "MAG_GLASS_SIZE" #define MAG_GLASS_ZOOM "MAG_GLASS_ZOOM" +#define MAG_GLASS_CIRCULAR "MAG_GLASS_CIRCULAR" +#define MAG_GLASS_RING "MAG_GLASS_RING" +#define MAG_GLASS_EDGE_EASE "MAG_GLASS_EDGE_EASE" #define ZOOM_LEVEL "ZOOM_LEVEL" #define SLIDE_SIZE "SLIDE_SIZE" #define GO_TO_FLOW_SIZE "GO_TO_FLOW_SIZE" @@ -41,6 +44,7 @@ #define USE_SINGLE_SCROLL_STEP_TO_TURN_PAGE "USE_SINGLE_SCROLL_STEP_TO_TURN_PAGE" #define DISABLE_SCROLL_ANIMATION "DISABLE_SCROLL_ANIMATION" #define MOUSE_MODE "MOUSE_MODE" +#define ESCAPE_KEY_BEHAVIOR "ESCAPE_KEY_BEHAVIOR" #define SCALING_METHOD "SCALING_METHOD" #define SAVE_RENDERED_PAGE_DIRECTORY "SAVE_RENDERED_PAGE_DIRECTORY" #define EXTRACT_PAGE_DIRECTORY "EXTRACT_PAGE_DIRECTORY" diff --git a/custom_widgets/CMakeLists.txt b/custom_widgets/CMakeLists.txt index 4e35aa059..cabc59989 100644 --- a/custom_widgets/CMakeLists.txt +++ b/custom_widgets/CMakeLists.txt @@ -11,6 +11,8 @@ set(WIDGETS_COMMON_SOURCES yacreader_field_plain_text_edit.cpp yacreader_options_dialog.h yacreader_options_dialog.cpp + yacreader_settings_widget.h + yacreader_settings_widget.cpp yacreader_spin_slider_widget.h yacreader_spin_slider_widget.cpp yacreader_tool_bar_stretch.h diff --git a/custom_widgets/rounded_corners_dialog.cpp b/custom_widgets/rounded_corners_dialog.cpp index f4e32e5e6..d6ae98abf 100644 --- a/custom_widgets/rounded_corners_dialog.cpp +++ b/custom_widgets/rounded_corners_dialog.cpp @@ -1,44 +1,71 @@ #include "rounded_corners_dialog.h" -#include +#include +#include +#include +#include -YACReader::RoundedCornersDialog::RoundedCornersDialog(QWidget *parent) - : QDialog(parent) +namespace { + +constexpr int cornerRadius = 14; + +QMargins marginsForEffect(const QGraphicsEffect &effect, const QSize &contentSize) { - setWindowFlags(windowFlags() | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); - setAttribute(Qt::WA_TranslucentBackground); + const QRectF contentBounds { QPointF(0, 0), QSizeF(contentSize) }; + const QRectF effectBounds = effect.boundingRectFor(contentBounds); + + return QMargins(qMax(0, qCeil(contentBounds.left() - effectBounds.left())), + qMax(0, qCeil(contentBounds.top() - effectBounds.top())), + qMax(0, qCeil(effectBounds.right() - contentBounds.right())), + qMax(0, qCeil(effectBounds.bottom() - contentBounds.bottom()))); } -void YACReader::RoundedCornersDialog::setBackgroundColor(const QColor &color) -{ - m_backgroundColor = color; - update(); } -void YACReader::RoundedCornersDialog::paintEvent(QPaintEvent *) +YACReader::RoundedCornersDialog::RoundedCornersDialog(QWidget *parent) + : QDialog(parent), m_dialogSurface(new QWidget(this)) { - qreal radius = 14.0; // desired radius in absolute pixels + setWindowFlags(windowFlags() | Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); + setAttribute(Qt::WA_TranslucentBackground); + + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_dialogSurface); - if (!(windowFlags() & Qt::FramelessWindowHint) && !testAttribute(Qt::WA_TranslucentBackground)) - return; // nothing to do + m_dialogSurface->setObjectName("roundedCornersDialogSurface"); + m_dialogSurface->setAttribute(Qt::WA_StyledBackground); - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); + auto shadow = new QGraphicsDropShadowEffect(m_dialogSurface); + shadow->setBlurRadius(36); + shadow->setColor(QColor(0, 0, 0, 180)); + shadow->setOffset(3, 6); + m_dialogSurface->setGraphicsEffect(shadow); - // Paint thyself. - QRectF rect(QPointF(0, 0), size()); + setBackgroundColor(Qt::white); +} + +QWidget *YACReader::RoundedCornersDialog::dialogSurface() const +{ + return m_dialogSurface; +} - p.setPen(Qt::NoPen); +void YACReader::RoundedCornersDialog::setContentFixedSize(const QSize &size) +{ + m_dialogSurface->setFixedSize(size); - // Set the brush from palette role. - // p.setBrush(palette().brush(backgroundRole())); - p.setBrush(QBrush(m_backgroundColor)); - // Got radius? Otherwise draw a quicker rect. - if (radius > 0.0) - p.drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize); - else - p.drawRect(rect); + const auto effect = m_dialogSurface->graphicsEffect(); + const QMargins shadowMargins = effect != nullptr ? marginsForEffect(*effect, size) : QMargins(); + layout()->setContentsMargins(shadowMargins); + setFixedSize(size + QSize(shadowMargins.left() + shadowMargins.right(), shadowMargins.top() + shadowMargins.bottom())); +} - // C'est finí - p.end(); +void YACReader::RoundedCornersDialog::setBackgroundColor(const QColor &color) +{ + m_dialogSurface->setStyleSheet(QString("QWidget#roundedCornersDialogSurface { background-color: rgba(%1, %2, %3, %4); border-radius: %5px; }") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()) + .arg(color.alpha()) + .arg(cornerRadius)); } diff --git a/custom_widgets/rounded_corners_dialog.h b/custom_widgets/rounded_corners_dialog.h index 3e5c3ce10..5d4aaf54e 100644 --- a/custom_widgets/rounded_corners_dialog.h +++ b/custom_widgets/rounded_corners_dialog.h @@ -4,6 +4,8 @@ #include #include +class QWidget; + namespace YACReader { class RoundedCornersDialog : public QDialog { @@ -12,11 +14,12 @@ class RoundedCornersDialog : public QDialog explicit RoundedCornersDialog(QWidget *parent = nullptr); protected: - void paintEvent(QPaintEvent *) override; + QWidget *dialogSurface() const; + void setContentFixedSize(const QSize &size); void setBackgroundColor(const QColor &color); private: - QColor m_backgroundColor { 255, 255, 255 }; + QWidget *m_dialogSurface; }; } diff --git a/custom_widgets/whats_new_dialog.cpp b/custom_widgets/whats_new_dialog.cpp index 14844f403..d67dbd6a5 100644 --- a/custom_widgets/whats_new_dialog.cpp +++ b/custom_widgets/whats_new_dialog.cpp @@ -36,14 +36,15 @@ QString renderInlineMarkdown(QString text) YACReader::WhatsNewDialog::WhatsNewDialog(QWidget *parent) : RoundedCornersDialog(parent) { - auto scrollArea = new QScrollArea(this); + auto surface = dialogSurface(); + auto scrollArea = new QScrollArea(surface); scrollArea->setStyleSheet("background-color:transparent;" "border:none;"); scrollArea->horizontalScrollBar()->setStyleSheet("QScrollBar {height:0px;}"); scrollArea->verticalScrollBar()->setStyleSheet("QScrollBar {width:0px;}"); scrollArea->setContentsMargins(0, 0, 0, 0); - auto mainLayout = new QVBoxLayout(this); + auto mainLayout = new QVBoxLayout(surface); mainLayout->setContentsMargins(0, 0, 0, 0); auto contentLayout = new QGridLayout(); @@ -87,9 +88,7 @@ YACReader::WhatsNewDialog::WhatsNewDialog(QWidget *parent) scrollArea->setWidget(content); scrollArea->setWidgetResizable(true); - this->setLayout(mainLayout); - - closeButton = new QPushButton(this); + closeButton = new QPushButton(surface); closeButton->setFlat(true); closeButton->setStyleSheet("background-color:transparent;"); closeButton->setIconSize(QSize(44, 44)); @@ -97,7 +96,7 @@ YACReader::WhatsNewDialog::WhatsNewDialog(QWidget *parent) closeButton->move(656, 20); scrollArea->setFixedSize(720, 640); - setFixedSize(720, 640); + setContentFixedSize(QSize(720, 640)); setModal(true); connect(closeButton, &QPushButton::clicked, this, &QDialog::close); @@ -272,7 +271,9 @@ QString YACReader::WhatsNewDialog::renderHtmlDocument(const QString &content) co QString YACReader::WhatsNewDialog::renderIntro() const { - return "YACReader 10.1 is here, with smoother reading, better page saving and exporting, Windows long path support, a refreshed server web UI and more:"; + return "YACReader 10.2 adds a new basic web reader, redesigned settings dialogs, and experimental EPUB support. " + "It also brings more natural zoom controls, a better magnifying glass with an option to make it round, and more. " + "Don't forget to check the new built-in search guide so you can make the most of the search engine."; } QString YACReader::WhatsNewDialog::renderFooter() const diff --git a/custom_widgets/yacreader_macosx_toolbar.h b/custom_widgets/yacreader_macosx_toolbar.h index d383865b6..e2df74c39 100644 --- a/custom_widgets/yacreader_macosx_toolbar.h +++ b/custom_widgets/yacreader_macosx_toolbar.h @@ -8,6 +8,8 @@ #include +class QMenu; + class YACReaderMacOSXSearchLineEdit : public YACReaderSearchLineEdit { }; @@ -27,7 +29,14 @@ class YACReaderMacOSXToolbar : public YACReaderMainToolBar void *getSearchEditDelegate() { return searchEditDelegate; }; - void emitFilterChange(const QString &filter) { emit filterChanged(filter); }; + void setNativeSearchField(void *field); + void setSearchMenu(QMenu *menu); + void setSearchText(const QString &text, bool notify = true); + void clearSearchText(bool notify = true); + void focusSearch(); + void setSearchEnabled(bool enabled); + QString searchText() const; + void nativeSearchTextChanged(const QString &text); QAction *actionFromIdentifier(const QString &identifier); signals: @@ -37,6 +46,11 @@ class YACReaderMacOSXToolbar : public YACReaderMainToolBar void paintEvent(QPaintEvent *) override; void *searchEditDelegate; + void *nativeSearchField; + QMenu *searchMenu; + YACReaderMacOSXSearchLineEdit *searchEditProxy; + QString pendingSearchText; + bool searchEnabled; }; #else diff --git a/custom_widgets/yacreader_macosx_toolbar.mm b/custom_widgets/yacreader_macosx_toolbar.mm index 483177ac5..e5bf036c1 100644 --- a/custom_widgets/yacreader_macosx_toolbar.mm +++ b/custom_widgets/yacreader_macosx_toolbar.mm @@ -233,6 +233,7 @@ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString searchItem.resignsFirstResponderWithCancel = true; searchItem.searchField.delegate = id(mytoolbar->getSearchEditDelegate()); searchItem.toolTip = @"Search"; + mytoolbar->setNativeSearchField(searchItem.searchField); return searchItem; } @@ -279,6 +280,7 @@ @interface YACReaderLibrarySearchDelegate : NSObject { @public YACReaderMacOSXToolbar *mytoolbar; } +- (IBAction)searchMenuItemClicked:(id)sender; @end @implementation YACReaderLibrarySearchDelegate @@ -293,15 +295,26 @@ - (void)searchFieldDidEndSearching:(NSSearchField *)sender - (void)controlTextDidChange:(NSNotification *)notification { NSSearchField *searchField = notification.object; - NSLog(@"Search text changed: %@", searchField.stringValue); - - mytoolbar->emitFilterChange(QString::fromNSString(searchField.stringValue)); + mytoolbar->nativeSearchTextChanged(QString::fromNSString(searchField.stringValue)); +} +- (IBAction)searchMenuItemClicked:(id)sender +{ + NSMenuItem *item = reinterpret_cast(sender); + NSValue *actionValue = item.representedObject; + auto *action = static_cast(actionValue.pointerValue); + if (action) + action->trigger(); } @end YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent) - : YACReaderMainToolBar(parent) + : YACReaderMainToolBar(parent), + searchEditDelegate(nullptr), + nativeSearchField(nullptr), + searchMenu(nullptr), + searchEditProxy(nullptr), + searchEnabled(true) { backButton->setIconSize(QSize(24, 24)); forwardButton->setIconSize(QSize(24, 24)); @@ -354,13 +367,111 @@ - (void)controlTextDidChange:(NSNotification *)notification YACReaderMacOSXSearchLineEdit *YACReaderMacOSXToolbar::addSearchEdit() { - auto search = new YACReaderMacOSXSearchLineEdit(); + auto *search = new YACReaderMacOSXSearchLineEdit(); + searchEditProxy = search; setSearchWidget(search); return search; } +void YACReaderMacOSXToolbar::setNativeSearchField(void *field) +{ + nativeSearchField = field; + + auto *nativeField = reinterpret_cast(nativeSearchField); + nativeField.stringValue = pendingSearchText.toNSString(); + nativeField.enabled = searchEnabled; + + setSearchMenu(searchMenu); +} + +void YACReaderMacOSXToolbar::setSearchMenu(QMenu *menu) +{ + searchMenu = menu; + if (!nativeSearchField || !searchMenu || !searchEditDelegate) + return; + + auto *nativeMenu = [[[NSMenu alloc] initWithTitle:@"Search filters"] autorelease]; + for (QAction *action : searchMenu->actions()) { + if (action->isSeparator() && action->text().isEmpty()) { + [nativeMenu addItem:NSMenuItem.separatorItem]; + continue; + } + + auto *item = [[[NSMenuItem alloc] + initWithTitle:action->text().toNSString() + action:action->isSeparator() ? nil : @selector(searchMenuItemClicked:) + keyEquivalent:@""] autorelease]; + item.enabled = !action->isSeparator() && action->isEnabled(); + item.target = action->isSeparator() ? nil : id(searchEditDelegate); + item.representedObject = [NSValue valueWithPointer:action]; + [nativeMenu addItem:item]; + } + + auto *nativeField = reinterpret_cast(nativeSearchField); + auto *cell = reinterpret_cast(nativeField.cell); + cell.searchMenuTemplate = nativeMenu; +} + +void YACReaderMacOSXToolbar::setSearchText(const QString &text, bool notify) +{ + pendingSearchText = text; + + if (nativeSearchField) { + auto *nativeField = reinterpret_cast(nativeSearchField); + nativeField.stringValue = text.toNSString(); + } + + if (searchEditProxy) { + const QSignalBlocker blocker(searchEditProxy); + searchEditProxy->setText(text); + } + + if (notify) + emit filterChanged(text); +} + +void YACReaderMacOSXToolbar::clearSearchText(bool notify) +{ + setSearchText({ }, notify); +} + +void YACReaderMacOSXToolbar::focusSearch() +{ + if (!nativeSearchField) + return; + + auto *nativeField = reinterpret_cast(nativeSearchField); + [nativeField.window makeFirstResponder:nativeField]; +} + +void YACReaderMacOSXToolbar::setSearchEnabled(bool enabled) +{ + searchEnabled = enabled; + if (nativeSearchField) { + auto *nativeField = reinterpret_cast(nativeSearchField); + nativeField.enabled = enabled; + } + if (searchEditProxy) + searchEditProxy->setEnabled(enabled); +} + +QString YACReaderMacOSXToolbar::searchText() const +{ + return pendingSearchText; +} + +void YACReaderMacOSXToolbar::nativeSearchTextChanged(const QString &text) +{ + pendingSearchText = text; + if (searchEditProxy) { + const QSignalBlocker blocker(searchEditProxy); + searchEditProxy->setText(text); + } + emit filterChanged(text); +} + void YACReaderMacOSXToolbar::updateViewSelectorIcon(const QIcon &icon) { } diff --git a/custom_widgets/yacreader_options_dialog.cpp b/custom_widgets/yacreader_options_dialog.cpp index 99c214cdd..ae16fdf72 100644 --- a/custom_widgets/yacreader_options_dialog.cpp +++ b/custom_widgets/yacreader_options_dialog.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include @@ -21,12 +23,18 @@ YACReaderOptionsDialog::YACReaderOptionsDialog(QWidget *parent) cancel->setDefault(true); - QVBoxLayout *shortcutsLayout = new QVBoxLayout(); - QPushButton *shortcutsButton = new QPushButton(tr("Edit shortcuts")); - shortcutsLayout->addWidget(shortcutsButton); - - shortcutsBox = new QGroupBox(tr("Shortcuts")); - shortcutsBox->setLayout(shortcutsLayout); + shortcutsPage = new QWidget(); + shortcutsPage->setWindowTitle(tr("Shortcuts")); + shortcutsLayout = new QVBoxLayout(shortcutsPage); + auto *shortcutsBox = new QGroupBox(tr("Keyboard shortcuts")); + auto *shortcutsBoxLayout = new QHBoxLayout(shortcutsBox); + auto *shortcutsDescription = new QLabel(tr("Customize the keyboard shortcuts used by the application.")); + auto *shortcutsButton = new QPushButton(tr("Edit shortcuts")); + shortcutsDescription->setWordWrap(true); + shortcutsBoxLayout->addWidget(shortcutsDescription, 1); + shortcutsBoxLayout->addWidget(shortcutsButton, 0, Qt::AlignRight); + shortcutsLayout->addWidget(shortcutsBox); + shortcutsLayout->addStretch(); connect(shortcutsButton, &QAbstractButton::clicked, this, &YACReaderOptionsDialog::editShortcuts); @@ -86,6 +94,11 @@ YACReaderOptionsDialog::YACReaderOptionsDialog(QWidget *parent) connect(gl->vSyncCheck, &QCheckBox::checkStateChanged, this, &YACReaderOptionsDialog::saveUseVSync); } +void YACReaderOptionsDialog::addShortcutsSection(QWidget *section) +{ + shortcutsLayout->insertWidget(shortcutsLayout->count() - 1, section); +} + void YACReaderOptionsDialog::savePerformance(int value) { settings->setValue(PERFORMANCE, value); diff --git a/custom_widgets/yacreader_options_dialog.h b/custom_widgets/yacreader_options_dialog.h index 44a0b9a90..93ddfd7df 100644 --- a/custom_widgets/yacreader_options_dialog.h +++ b/custom_widgets/yacreader_options_dialog.h @@ -7,7 +7,7 @@ class YACReader3DFlowConfigWidget; class QCheckBox; class QPushButton; class QSettings; -class QGroupBox; +class QVBoxLayout; class YACReaderOptionsDialog : public QDialog { @@ -18,11 +18,14 @@ class YACReaderOptionsDialog : public QDialog QPushButton *accept; QPushButton *cancel; - QGroupBox *shortcutsBox; + QWidget *shortcutsPage; + QVBoxLayout *shortcutsLayout; QSettings *settings; QSettings *previousSettings; + void addShortcutsSection(QWidget *section); + public: YACReaderOptionsDialog(QWidget *parent); public slots: diff --git a/custom_widgets/yacreader_search_line_edit.cpp b/custom_widgets/yacreader_search_line_edit.cpp index 8090f2e01..0def7ac89 100644 --- a/custom_widgets/yacreader_search_line_edit.cpp +++ b/custom_widgets/yacreader_search_line_edit.cpp @@ -1,6 +1,8 @@ #include "yacreader_search_line_edit.h" #include +#include +#include #include #include @@ -8,9 +10,18 @@ YACReaderSearchLineEdit::YACReaderSearchLineEdit(QWidget *parent) : QLineEdit(parent), paddingLeft(0), paddingRight(0) { clearButton = new QToolButton(this); + menuButton = new QToolButton(this); searchLabel = new QLabel(this); clearButton->setIconSize(QSize(12, 12)); + menuButton->setAutoRaise(true); + menuButton->setFixedSize(18, 18); + menuButton->setIconSize(QSize(10, 6)); + menuButton->setCursor(Qt::ArrowCursor); + menuButton->setPopupMode(QToolButton::InstantPopup); + menuButton->setToolButtonStyle(Qt::ToolButtonIconOnly); + menuButton->setToolTip(tr("Search filters")); + menuButton->hide(); clearButton->setCursor(Qt::ArrowCursor); clearButton->hide(); @@ -35,6 +46,12 @@ YACReaderSearchLineEdit::YACReaderSearchLineEdit(QWidget *parent) initTheme(this); } +void YACReaderSearchLineEdit::setSearchMenu(QMenu *menu) +{ + menuButton->setMenu(menu); + menuButton->setVisible(menu != nullptr && QLineEdit::text().isEmpty()); +} + void YACReaderSearchLineEdit::applyTheme(const Theme &theme) { const auto &searchTheme = theme.searchLineEdit; @@ -42,9 +59,32 @@ void YACReaderSearchLineEdit::applyTheme(const Theme &theme) setStyleSheet(searchTheme.lineEditQSS.arg(paddingLeft).arg(paddingRight)); searchLabel->setStyleSheet(searchTheme.searchLabelQSS); clearButton->setStyleSheet(searchTheme.clearButtonQSS); + menuButton->setStyleSheet(QStringLiteral( + "QToolButton { border: none; padding: 0px; }" + "QToolButton::menu-indicator { image: none; width: 0px; }")); searchLabel->setPixmap(searchTheme.searchIcon); clearButton->setIcon(QIcon(searchTheme.clearIcon)); + + const qreal dpr = devicePixelRatioF(); + QPixmap chevron(qCeil(10 * dpr), qCeil(6 * dpr)); + chevron.setDevicePixelRatio(dpr); + chevron.fill(Qt::transparent); + + QPainter painter(&chevron); + painter.setRenderHint(QPainter::Antialiasing); + QPen pen(searchTheme.iconColor); + pen.setWidthF(1.4); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + painter.setPen(pen); + painter.drawPolyline(QPolygonF { + QPointF(1, 1), + QPointF(5, 5), + QPointF(9, 1) }); + painter.end(); + + menuButton->setIcon(QIcon(chevron)); } void YACReaderSearchLineEdit::clearText() @@ -61,11 +101,13 @@ const QString YACReaderSearchLineEdit::text() void YACReaderSearchLineEdit::resizeEvent(QResizeEvent *) { - QSize sz = clearButton->sizeHint(); - int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); - int marginRight = style()->pixelMetric(QStyle::PM_LayoutRightMargin); - clearButton->move(rect().right() - frameWidth - sz.width() - marginRight - 6, - (rect().bottom() + 2 - sz.height()) / 2); + const QSize clearSize = clearButton->sizeHint(); + const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + const int marginRight = style()->pixelMetric(QStyle::PM_LayoutRightMargin); + const int menuX = rect().right() - frameWidth - menuButton->width() - marginRight - 6; + menuButton->move(menuX, (height() - menuButton->height()) / 2); + clearButton->move(rect().right() - frameWidth - clearSize.width() - marginRight - 6, + (rect().bottom() + 2 - clearSize.height()) / 2); QSize szl = searchLabel->sizeHint(); searchLabel->move(8, (rect().bottom() + 2 - szl.height()) / 2); @@ -74,6 +116,7 @@ void YACReaderSearchLineEdit::resizeEvent(QResizeEvent *) void YACReaderSearchLineEdit::updateCloseButton(const QString &text) { clearButton->setVisible(!text.isEmpty()); + menuButton->setVisible(menuButton->menu() != nullptr && text.isEmpty()); } void YACReaderSearchLineEdit::processText(const QString &text) diff --git a/custom_widgets/yacreader_search_line_edit.h b/custom_widgets/yacreader_search_line_edit.h index bca2b9a42..b9b195503 100644 --- a/custom_widgets/yacreader_search_line_edit.h +++ b/custom_widgets/yacreader_search_line_edit.h @@ -8,6 +8,7 @@ class QToolButton; class QLabel; +class QMenu; class YACReaderSearchLineEdit : public QLineEdit, protected Themable { @@ -17,6 +18,7 @@ class YACReaderSearchLineEdit : public QLineEdit, protected Themable YACReaderSearchLineEdit(QWidget *parent = 0); void clearText(); // no signal emited; const QString text(); + void setSearchMenu(QMenu *menu); protected: void resizeEvent(QResizeEvent *); @@ -31,6 +33,7 @@ private slots: private: QToolButton *clearButton; + QToolButton *menuButton; QLabel *searchLabel; int paddingLeft; diff --git a/custom_widgets/yacreader_settings_widget.cpp b/custom_widgets/yacreader_settings_widget.cpp new file mode 100644 index 000000000..97ba88fef --- /dev/null +++ b/custom_widgets/yacreader_settings_widget.cpp @@ -0,0 +1,75 @@ +#include "yacreader_settings_widget.h" + +#include +#include +#include +#include +#include + +namespace { +constexpr int navigationMinimumWidth = 140; +constexpr int navigationHorizontalPadding = 32; +constexpr int minimumContentWidth = 400; +constexpr int defaultContentWidth = 530; +} + +YACReaderSettingsWidget::YACReaderSettingsWidget(QWidget *parent) + : QWidget(parent), navigation(new QListWidget(this)), pages(new QStackedWidget(this)), splitter(new QSplitter(Qt::Horizontal, this)) +{ + navigation->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + navigation->setMinimumWidth(navigationMinimumWidth); + navigation->setSelectionMode(QAbstractItemView::SingleSelection); + navigation->setUniformItemSizes(true); + + navigation->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + pages->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + splitter->addWidget(navigation); + splitter->addWidget(pages); + splitter->setCollapsible(0, false); + splitter->setCollapsible(1, false); + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + + auto *layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(splitter); + + connect(navigation, &QListWidget::currentRowChanged, pages, &QStackedWidget::setCurrentIndex); +} + +int YACReaderSettingsWidget::addPage(QWidget *page, const QString &title, const QIcon &icon) +{ + const int index = pages->addWidget(page); + navigation->addItem(new QListWidgetItem(icon, title)); + updateNavigationSize(); + updateGeometry(); + + if (navigation->currentRow() == -1) + navigation->setCurrentRow(0); + + return index; +} + +QSize YACReaderSettingsWidget::sizeHint() const +{ + const int width = preferredNavigationWidth() + splitter->handleWidth() + preferredContentWidth(); + const int height = qMax(pages->sizeHint().height(), navigation->sizeHint().height()); + + return QSize(width, height); +} + +int YACReaderSettingsWidget::preferredNavigationWidth() const +{ + return qMax(navigationMinimumWidth, navigation->sizeHintForColumn(0) + navigationHorizontalPadding); +} + +int YACReaderSettingsWidget::preferredContentWidth() const +{ + return qBound(minimumContentWidth, pages->sizeHint().width(), defaultContentWidth); +} + +void YACReaderSettingsWidget::updateNavigationSize() +{ + splitter->setSizes({ preferredNavigationWidth(), preferredContentWidth() }); +} diff --git a/custom_widgets/yacreader_settings_widget.h b/custom_widgets/yacreader_settings_widget.h new file mode 100644 index 000000000..9ed6012f3 --- /dev/null +++ b/custom_widgets/yacreader_settings_widget.h @@ -0,0 +1,32 @@ +#ifndef YACREADER_SETTINGS_WIDGET_H +#define YACREADER_SETTINGS_WIDGET_H + +#include +#include +#include +#include + +class QListWidget; +class QSplitter; +class QStackedWidget; + +class YACReaderSettingsWidget : public QWidget +{ +public: + explicit YACReaderSettingsWidget(QWidget *parent = nullptr); + + int addPage(QWidget *page, const QString &title, const QIcon &icon = { }); + + QSize sizeHint() const override; + +private: + int preferredNavigationWidth() const; + int preferredContentWidth() const; + void updateNavigationSize(); + + QListWidget *navigation; + QStackedWidget *pages; + QSplitter *splitter; +}; + +#endif // YACREADER_SETTINGS_WIDGET_H diff --git a/images/chevronDown.svg b/images/chevronDown.svg new file mode 100644 index 000000000..20c4a2363 --- /dev/null +++ b/images/chevronDown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/release/server/docroot/css/webui.css b/release/server/docroot/css/webui.css index 9607dedb0..c1e3562f4 100644 --- a/release/server/docroot/css/webui.css +++ b/release/server/docroot/css/webui.css @@ -92,6 +92,7 @@ code { height: 100vh; flex: 0 0 252px; flex-direction: column; + overflow-y: auto; padding: 22px 16px; border-right: 1px solid var(--border); background: var(--surface); @@ -189,11 +190,470 @@ input[type="time"]:focus-visible { font-weight: 600; } +.sidebar-promos { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: auto; + padding-top: 22px; +} + +.sidebar-mobile-promo { + position: relative; + overflow: hidden; + padding: 14px; + border: 1px solid var(--border); + border-radius: 14px; + background: radial-gradient(130% 95% at 100% 0%, var(--accent-soft), transparent 58%), var(--surface-subtle); +} + +.sidebar-mobile-promo::after { + position: absolute; + top: -42px; + right: -48px; + width: 110px; + height: 110px; + border: 1px solid color-mix(in srgb, var(--accent) 16%, transparent); + border-radius: 50%; + content: ""; + pointer-events: none; +} + +.sidebar-mobile-header { + display: flex; + position: relative; + z-index: 1; + align-items: center; + gap: 10px; +} + +.sidebar-mobile-promo h2 { + margin: 0; + font-size: 15px; + letter-spacing: -0.015em; + line-height: 1.2; +} + +.sidebar-mobile-promo p { + margin: 6px 0 0; + color: var(--text-muted); + font-size: 11px; + line-height: 1.45; +} + +.store-links { + display: flex; + position: relative; + z-index: 1; + flex-direction: column; + align-items: center; + gap: 10px; + margin-top: 13px; +} + +.store-badge-link { + display: block; + border-radius: 7px; + line-height: 0; +} + +.store-badge-link:focus-visible, +.donate-button:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 2px; +} + +.store-badge-link img { + display: block; + max-width: 100%; +} + +.app-store-badge img { + height: 40px; + width: auto; +} + +.google-play-badge img { + width: 151px; + height: auto; +} + +.mobile-promotion-open { + overflow: hidden; +} + +.mobile-promotion-overlay { + display: grid; + position: fixed; + z-index: 100; + inset: 0; + overflow-y: auto; + padding: 24px; + background: rgba(7, 7, 8, 0.74); + place-items: center; +} + +.mobile-promotion-dialog { + position: relative; + width: min(760px, 100%); + max-height: calc(100vh - 48px); + overflow-y: auto; + padding: 30px; + border: 1px solid var(--border-strong); + border-radius: 20px; + background: + radial-gradient(90% 70% at 100% 0%, var(--accent-soft), transparent 58%), + var(--surface); + box-shadow: 0 28px 80px rgba(0, 0, 0, 0.42); + color: var(--text); +} + +.mobile-promotion-close { + display: grid; + position: absolute; + top: 16px; + right: 16px; + width: 36px; + height: 36px; + padding: 0; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface-subtle); + color: var(--text-muted); + cursor: pointer; + place-items: center; +} + +.mobile-promotion-close:hover { + border-color: var(--border-strong); + color: var(--text); +} + +.mobile-promotion-close:focus-visible, +.mobile-platform-website:focus-visible, +.mobile-promotion-continue:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 2px; +} + +.mobile-promotion-close-icon { + width: 17px; + height: 17px; +} + +.mobile-promotion-intro { + max-width: 620px; + padding-right: 34px; +} + +.mobile-promotion-intro h2 { + margin: 0; + font-size: clamp(25px, 4vw, 34px); + letter-spacing: -0.035em; + line-height: 1.05; +} + +.mobile-promotion-intro p { + margin: 10px 0 0; + color: var(--text-muted); + font-size: 14px; + line-height: 1.55; +} + +.mobile-promotion-features { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 18px; + margin: 24px 0; + padding: 0; + list-style: none; +} + +.mobile-promotion-features li { + display: flex; + align-items: flex-start; + gap: 9px; +} + +.mobile-promotion-check { + width: 17px; + height: 17px; + flex: none; + margin-top: 1px; + color: var(--accent-strong); + stroke-width: 2.4; +} + +.mobile-promotion-feature-copy { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +} + +.mobile-promotion-feature-copy strong { + color: var(--text); + font-size: 12px; +} + +.mobile-promotion-feature-copy span { + color: var(--text-muted); + font-size: 11.5px; + line-height: 1.45; +} + +.mobile-promotion-more { + margin: -10px 0 18px 26px; + color: var(--text-muted); + font-size: 11.5px; + line-height: 1.45; +} + +.mobile-promotion-more strong { + color: var(--text); +} + +.mobile-platform-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.mobile-platform-card { + display: flex; + min-width: 0; + flex-direction: column; + align-items: flex-start; + gap: 13px; + padding: 18px; + border: 1px solid var(--border); + border-radius: 15px; + background: color-mix(in srgb, var(--surface-subtle) 88%, transparent); +} + +.mobile-platform-copy h3 { + margin: 0; + font-size: 16px; +} + +.mobile-platform-copy span { + display: block; + margin-top: 4px; + color: var(--text-muted); + font-size: 11.5px; +} + +.mobile-platform-website { + display: inline-flex; + align-items: center; + gap: 5px; + color: var(--accent-strong); + font-size: 11.5px; + font-weight: 700; +} + +.mobile-platform-website:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.mobile-platform-link-icon { + width: 14px; + height: 14px; +} + +.mobile-platform-card .store-badge-link { + margin-top: auto; +} + +.mobile-promotion-continue { + display: block; + min-width: 190px; + margin: 22px auto 0; +} + +.sidebar-donation { + display: flex; + flex-direction: column; + gap: 10px; + padding: 13px 14px 14px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface-subtle); +} + +.sidebar-donation-copy { + display: flex; + flex-direction: column; + gap: 3px; +} + +.sidebar-donation-copy strong { + font-size: 12px; +} + +.sidebar-donation-copy span { + color: var(--text-muted); + font-size: 10.5px; +} + +.donate-button { + display: flex; + min-height: 39px; + align-items: center; + justify-content: center; + gap: 8px; + padding: 9px 12px; + border: 1px solid color-mix(in srgb, var(--accent-hover) 65%, transparent); + border-radius: 10px; + background: linear-gradient(180deg, #ffc83d, var(--accent)); + box-shadow: 0 5px 14px rgba(220, 146, 0, 0.2); + color: #2b1b00; + font-size: 12px; + font-weight: 800; + transition: filter 150ms ease, transform 150ms ease, box-shadow 150ms ease; +} + +.donate-button:hover { + filter: brightness(1.04); + transform: translateY(-1px); + box-shadow: 0 7px 18px rgba(220, 146, 0, 0.26); +} + +.donate-button-icon { + width: 15px; + height: 15px; +} + +.compact-promo-actions { + display: none; +} + +.compact-promo-button { + display: grid; + width: 38px; + height: 38px; + padding: 0; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); + color: var(--text-dim); + cursor: pointer; + place-items: center; + transition: border-color 150ms ease, background 150ms ease, color 150ms ease, transform 150ms ease; +} + +.compact-promo-button:hover { + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + background: var(--accent-soft); + color: var(--accent-strong); + transform: translateY(-1px); +} + +.compact-promo-button:focus-visible, +.compact-popover-close:focus-visible { + outline: 3px solid var(--accent-soft); + outline-offset: 2px; +} + +.compact-donate-button { + color: var(--accent-strong); +} + +.compact-promo-icon { + width: 17px; + height: 17px; +} + +.compact-mobile-popover { + position: absolute; + z-index: 30; + top: calc(100% + 10px); + right: -46px; + width: min(280px, calc(100vw - 36px)); + padding: 16px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28); +} + +.compact-mobile-popover[hidden] { + display: none; +} + +.compact-mobile-popover::before { + position: absolute; + top: -6px; + right: 59px; + width: 10px; + height: 10px; + border-top: 1px solid var(--border); + border-left: 1px solid var(--border); + background: var(--surface); + content: ""; + transform: rotate(45deg); +} + +.compact-mobile-popover-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.compact-mobile-popover h2 { + margin: 0; + font-size: 16px; + letter-spacing: -0.015em; +} + +.compact-mobile-popover p { + margin: 8px 0 14px; + color: var(--text-muted); + font-size: 11.5px; + line-height: 1.5; +} + +.compact-mobile-popover > .store-badge-link { + width: max-content; + margin-right: auto; + margin-left: auto; +} + +.compact-mobile-popover > .store-badge-link + .store-badge-link { + margin-top: 10px; +} + +.compact-popover-close { + display: grid; + width: 28px; + height: 28px; + flex: none; + padding: 0; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--text-muted); + cursor: pointer; + place-items: center; +} + +.compact-popover-close:hover { + background: var(--surface-subtle); + color: var(--text); +} + +.compact-popover-close-icon { + width: 16px; + height: 16px; +} + .server-summary { display: flex; align-items: center; gap: 8px; - margin-top: auto; + margin-top: 12px; padding: 14px 8px 2px; border-top: 1px solid var(--border); color: var(--text-muted); @@ -1157,7 +1617,8 @@ input[type="time"]:focus-visible { line-height: 1.35; } -.comic-synopsis a { +.comic-copy-section a, +.comic-metadata-field a { color: var(--accent-strong); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 55%, transparent); @@ -1280,66 +1741,251 @@ input[type="time"]:focus-visible { transform: translateY(-1px); } -.comic-back-button { +.comic-read-button { width: 100%; margin-top: 18px; } -.mobile-app-promo { - margin-top: 14px; - padding: 17px; - border: 1px solid var(--border); - border-radius: 12px; - background: radial-gradient(120% 140% at 100% 0%, var(--accent-soft), transparent 52%), var(--surface); - box-shadow: var(--shadow); +.comic-mobile-compact-actions { + position: relative; + margin-top: 8px; } -.mobile-app-promo h3 { - margin: 6px 0 7px; - font-size: 14px; - letter-spacing: -0.01em; +.comic-read-mobile-button { + display: flex; + width: 100%; + min-height: 42px; + align-items: center; + justify-content: center; + gap: 8px; } -.mobile-app-promo p { - margin: 0; - color: var(--text-muted); - font-size: 11.5px; - line-height: 1.5; +.comic-read-mobile-icon { + width: 16px; + height: 16px; } -.mobile-app-links { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - margin-top: 14px; +.comic-mobile-popover { + top: calc(100% + 8px); + right: auto; + left: 0; + width: 100%; +} + +.comic-mobile-popover::before { + right: auto; + left: calc(50% - 5px); +} + +.web-reader-active { + overflow: hidden; +} + +.web-reader-active .sidebar, +.web-reader-active .browser-topbar { + display: none; +} + +.web-reader-active .main { + height: 100vh; + overflow: hidden; +} + +.web-reader-active .browser-content { + width: 100%; + max-width: none; + height: 100vh; + padding: 0; } -.mobile-app-link { +.web-reader { display: flex; - min-width: 0; - min-height: 36px; + width: 100%; + height: 100vh; + flex-direction: column; + background: #111; + color: #f7f7f7; +} + +.web-reader-toolbar { + display: grid; + min-height: 58px; + grid-template-columns: minmax(82px, auto) minmax(0, 1fr) minmax(72px, auto); + align-items: center; + gap: 16px; + padding: 8px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + background: #1b1b1b; +} + +.reader-toolbar-button { + display: inline-flex; + min-height: 38px; align-items: center; justify-content: center; - padding: 8px 9px; - border: 1px solid var(--border); + gap: 7px; + padding: 8px 11px; + border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 9px; - background: var(--surface-subtle); - color: var(--text-dim); - font-size: 11px; + background: #252525; + color: #f7f7f7; + cursor: pointer; + font-size: 12px; font-weight: 700; +} + +.reader-toolbar-button:hover { + border-color: rgba(255, 255, 255, 0.28); + background: #303030; +} + +.reader-toolbar-button:focus-visible, +.reader-page-button:focus-visible { + outline: 3px solid rgba(245, 167, 5, 0.45); + outline-offset: -3px; +} + +.reader-toolbar-icon { + width: 17px; + height: 17px; +} + +.web-reader-heading { + min-width: 0; text-align: center; - transition: border-color 150ms ease, color 150ms ease, transform 150ms ease; } -.mobile-app-link:hover { - border-color: var(--accent); - color: var(--accent-strong); - transform: translateY(-1px); +.web-reader-title { + overflow: hidden; + font-size: 13px; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; } -.mobile-app-link:focus-visible { - outline: 3px solid var(--accent-soft); - outline-offset: 2px; +.web-reader-page-indicator { + min-width: 72px; + color: #b8b8b8; + font-family: "Cascadia Mono", "SFMono-Regular", Consolas, monospace; + font-size: 12px; + font-weight: 600; + text-align: right; +} + +.web-reader-stage { + display: flex; + position: relative; + min-height: 0; + flex: 1; + background: #0d0d0d; +} + +.web-reader-image-frame { + display: flex; + position: relative; + width: 100%; + min-width: 0; + min-height: 0; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.web-reader-page { + display: block; + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.web-reader-page[hidden] { + display: none; +} + +.reader-page-button { + display: grid; + position: absolute; + z-index: 3; + top: 0; + bottom: 0; + width: clamp(48px, 7vw, 88px); + padding: 0; + border: 0; + background: transparent; + color: rgba(255, 255, 255, 0.72); + cursor: pointer; + place-items: center; +} + +.reader-previous-button { + left: 0; +} + +.reader-next-button { + right: 0; +} + +.reader-page-button:hover:not(:disabled) { + background: linear-gradient(to right, rgba(0, 0, 0, 0.34), transparent); + color: #fff; +} + +.reader-next-button:hover:not(:disabled) { + background: linear-gradient(to left, rgba(0, 0, 0, 0.34), transparent); +} + +.reader-page-button:disabled { + color: rgba(255, 255, 255, 0.16); + cursor: default; +} + +.reader-page-button-icon { + width: 26px; + height: 26px; +} + +.web-reader-loading, +.web-reader-error { + display: flex; + position: absolute; + z-index: 2; + inset: 0; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 11px; + background: rgba(13, 13, 13, 0.84); + color: #bdbdbd; + font-size: 12px; + text-align: center; +} + +.web-reader-loading[hidden], +.web-reader-error[hidden] { + display: none; +} + +.web-reader-error span { + max-width: 280px; + color: #888; + line-height: 1.45; +} + +.web-reader-error .secondary-button { + border-color: rgba(255, 255, 255, 0.18); + background: #252525; + color: #eee; +} + +.web-reader-spinner { + width: 24px; + height: 24px; + border: 2px solid rgba(255, 255, 255, 0.18); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 750ms linear infinite; } .settings-content { @@ -1724,9 +2370,11 @@ input[type="time"]:disabled { .sidebar { position: relative; + z-index: 20; width: 100%; height: auto; flex-basis: auto; + overflow: visible; padding: 14px 18px; border-right: 0; border-bottom: 1px solid var(--border); @@ -1754,6 +2402,18 @@ input[type="time"]:disabled { justify-content: center; } + .sidebar-promos { + display: none; + } + + .compact-promo-actions { + display: flex; + position: relative; + align-items: center; + gap: 6px; + margin-left: auto; + } + .server-summary { display: none; } @@ -1788,6 +2448,77 @@ input[type="time"]:disabled { } @media (max-width: 560px) { + .mobile-promotion-overlay { + align-items: start; + padding: 12px; + } + + .mobile-promotion-dialog { + max-height: calc(100vh - 24px); + padding: 22px 18px 18px; + border-radius: 16px; + } + + .mobile-promotion-close { + top: 12px; + right: 12px; + } + + .mobile-promotion-intro { + padding-right: 38px; + } + + .mobile-promotion-intro h2 { + font-size: 25px; + } + + .mobile-promotion-features, + .mobile-platform-grid { + grid-template-columns: 1fr; + } + + .mobile-promotion-features { + margin: 20px 0; + } + + .mobile-platform-card { + padding: 15px; + } + + .mobile-promotion-continue { + width: 100%; + } + + .web-reader-toolbar { + min-height: 52px; + grid-template-columns: 42px minmax(0, 1fr) 58px; + gap: 9px; + padding: 6px 8px; + } + + .reader-exit-button { + min-width: 38px; + padding: 0; + } + + .reader-exit-button span { + display: none; + } + + .web-reader-page-indicator { + min-width: 58px; + font-size: 11px; + } + + .reader-page-button { + width: 48px; + } + + .reader-page-button-icon { + width: 22px; + height: 22px; + } + .status-card { align-items: flex-start; padding: 26px 24px; @@ -1833,10 +2564,6 @@ input[type="time"]:disabled { margin: 0 auto; } - .mobile-app-promo { - width: 100%; - } - .comic-detail-copy > h2 { font-size: 30px; } diff --git a/release/server/docroot/images/webui/app-store-badge.svg b/release/server/docroot/images/webui/app-store-badge.svg new file mode 100644 index 000000000..072b425a1 --- /dev/null +++ b/release/server/docroot/images/webui/app-store-badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/release/server/docroot/images/webui/google-play-badge.png b/release/server/docroot/images/webui/google-play-badge.png new file mode 100644 index 000000000..131f3acaa Binary files /dev/null and b/release/server/docroot/images/webui/google-play-badge.png differ diff --git a/release/server/docroot/js/webui.js b/release/server/docroot/js/webui.js index ef4b53362..f5f4373bd 100644 --- a/release/server/docroot/js/webui.js +++ b/release/server/docroot/js/webui.js @@ -35,6 +35,362 @@ return node; } + function svgIcon(className, iconMarkup) { + var icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + icon.setAttribute("class", className); + icon.setAttribute("viewBox", "0 0 24 24"); + icon.setAttribute("fill", "none"); + icon.setAttribute("stroke", "currentColor"); + icon.setAttribute("stroke-width", "1.8"); + icon.setAttribute("stroke-linecap", "round"); + icon.setAttribute("stroke-linejoin", "round"); + icon.setAttribute("aria-hidden", "true"); + icon.innerHTML = iconMarkup; + return icon; + } + + function storeBadge(className, imageSource, href, ariaLabel) { + var link = element("a", "store-badge-link " + className); + link.href = href; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.setAttribute("aria-label", ariaLabel); + + var badge = element("img"); + badge.src = imageSource; + badge.alt = ""; + link.appendChild(badge); + return link; + } + + var firstReaderPromotionShown = false; + + function showFirstReaderPromotion(returnFocus) { + if (firstReaderPromotionShown) { + return; + } + + try { + if (localStorage.getItem("yacreader-webui-reader-promotion-v2") === "shown") { + firstReaderPromotionShown = true; + return; + } + localStorage.setItem("yacreader-webui-reader-promotion-v2", "shown"); + } catch (error) { + } + firstReaderPromotionShown = true; + + var overlay = element("div", "mobile-promotion-overlay"); + var dialog = element("section", "mobile-promotion-dialog"); + dialog.setAttribute("role", "dialog"); + dialog.setAttribute("aria-modal", "true"); + dialog.setAttribute("aria-labelledby", "first-comic-promotion-title"); + dialog.setAttribute("aria-describedby", "first-comic-promotion-copy"); + + var closeButton = element("button", "mobile-promotion-close"); + closeButton.type = "button"; + closeButton.setAttribute("aria-label", "Close mobile app promotion"); + closeButton.appendChild(svgIcon("mobile-promotion-close-icon", '')); + + var intro = element("header", "mobile-promotion-intro"); + var title = element("h2", "", "Upgrade your reading"); + title.id = "first-comic-promotion-title"; + var introCopy = element( + "p", + "", + "The best YACReader reading experience is on iOS and Android. Go far beyond the basic web reader with a fast, deeply customizable reader engineered for comics, manga and webtoons." + ); + introCopy.id = "first-comic-promotion-copy"; + intro.append(title, introCopy); + + var featureList = element("ul", "mobile-promotion-features"); + [ + [ + "Guided, panel by panel", + "Automatic panel detection, configurable framing and optional full-page stops." + ], + [ + "A layout for every comic", + "Fit and fill modes, single or double pages, manga direction, auto-scroll and continuous webtoon reading." + ], + [ + "Make every page look its best", + "Automatic margin trimming and powerful image filters for faded colors, dark scans and imperfect pages." + ], + [ + "Your library, everywhere", + "Browse and stream remotely, import for offline reading, and keep progress and settings synchronized." + ] + ].forEach(function (feature) { + var item = element("li"); + var featureCopy = element("span", "mobile-promotion-feature-copy"); + featureCopy.append(element("strong", "", feature[0]), element("span", "", feature[1])); + item.append( + svgIcon("mobile-promotion-check", ''), + featureCopy + ); + featureList.appendChild(item); + }); + + var moreFeatures = element("p", "mobile-promotion-more"); + moreFeatures.append( + element("strong", "", "And much more."), + document.createTextNode(" Discover the complete feature set for your device.") + ); + + function platformCard(platform, devices, website, storeLink) { + var card = element("section", "mobile-platform-card"); + var cardCopy = element("div", "mobile-platform-copy"); + cardCopy.append(element("h3", "", platform), element("span", "", devices)); + + var websiteLink = element("a", "mobile-platform-website", "Discover every " + platform + " feature"); + websiteLink.href = website; + websiteLink.target = "_blank"; + websiteLink.rel = "noopener noreferrer"; + websiteLink.appendChild(svgIcon("mobile-platform-link-icon", '')); + + card.append(cardCopy, websiteLink, storeLink); + return card; + } + + var platforms = element("div", "mobile-platform-grid"); + platforms.append( + platformCard( + "iOS", + "iPhone and iPad", + "https://ios.yacreader.com/", + storeBadge( + "app-store-badge", + "/images/webui/app-store-badge.svg", + "https://apps.apple.com/app/id635717885", + "Download YACReader on the App Store" + ) + ), + platformCard( + "Android", + "Phones, tablets and desktop layouts", + "https://android.yacreader.com/", + storeBadge( + "google-play-badge", + "/images/webui/google-play-badge.png", + "https://play.google.com/store/apps/details?id=com.yacreader.yacreader", + "Get YACReader on Google Play" + ) + ) + ); + + var continueButton = element("button", "secondary-button mobile-promotion-continue", "Continue on the web"); + continueButton.type = "button"; + + dialog.append(closeButton, intro, featureList, moreFeatures, platforms, continueButton); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + document.body.classList.add("mobile-promotion-open"); + + function closePromotion() { + document.removeEventListener("keydown", handlePromotionKeys); + document.body.classList.remove("mobile-promotion-open"); + overlay.remove(); + if (returnFocus && returnFocus.isConnected) { + returnFocus.focus(); + } + } + + function handlePromotionKeys(event) { + if (event.key === "Escape") { + event.preventDefault(); + closePromotion(); + return; + } + if (event.key !== "Tab") { + return; + } + + var focusable = Array.from(dialog.querySelectorAll("a[href], button:not([disabled])")); + if (!focusable.length) { + return; + } + var first = focusable[0]; + var last = focusable[focusable.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + + closeButton.addEventListener("click", closePromotion); + continueButton.addEventListener("click", closePromotion); + overlay.addEventListener("pointerdown", function (event) { + if (event.target === overlay) { + closePromotion(); + } + }); + document.addEventListener("keydown", handlePromotionKeys); + closeButton.focus(); + } + + function initSidebarActions() { + var sidebar = document.querySelector(".sidebar"); + var serverSummary = sidebar && sidebar.querySelector(".server-summary"); + if (!sidebar || !serverSummary) { + return; + } + + var promos = element("div", "sidebar-promos"); + + var mobilePromo = element("section", "sidebar-mobile-promo"); + mobilePromo.setAttribute("aria-labelledby", "mobile-promo-title"); + var mobileHeader = element("div", "sidebar-mobile-header"); + var mobileCopy = element("div"); + var mobileTitle = element("h2", "", "Upgrade your reading"); + mobileTitle.id = "mobile-promo-title"; + mobileCopy.append(mobileTitle, element("p", "", "Unlock a richer reading experience with panel-by-panel navigation, flexible fit modes, image filters, fluid animations, and much more.")); + mobileHeader.appendChild(mobileCopy); + + var storeLinks = element("nav", "store-links"); + storeLinks.setAttribute("aria-label", "Get the YACReader mobile apps"); + storeLinks.append( + storeBadge( + "app-store-badge", + "/images/webui/app-store-badge.svg", + "https://apps.apple.com/app/id635717885", + "Download YACReader on the App Store" + ), + storeBadge( + "google-play-badge", + "/images/webui/google-play-badge.png", + "https://play.google.com/store/apps/details?id=com.yacreader.yacreader", + "Get YACReader on Google Play" + ) + ); + mobilePromo.append(mobileHeader, storeLinks); + + var donation = element("section", "sidebar-donation"); + var donationCopy = element("div", "sidebar-donation-copy"); + donationCopy.append(element("strong", "", "Love YACReader?"), element("span", "", "Help keep it independent.")); + var donateLink = element("a", "donate-button"); + donateLink.href = "https://www.paypal.com/donate?business=5TAMNQCDDMVP8&item_name=Support+YACReader"; + donateLink.target = "_blank"; + donateLink.rel = "noopener noreferrer"; + donateLink.append( + svgIcon("donate-button-icon", ''), + element("span", "", "Donate") + ); + donation.append(donationCopy, donateLink); + + promos.append(mobilePromo, donation); + sidebar.insertBefore(promos, serverSummary); + + var sidebarHeader = sidebar.querySelector(".sidebar-header"); + var themeToggle = sidebar.querySelector("[data-theme-toggle]"); + if (!sidebarHeader || !themeToggle) { + return; + } + + var compactActions = element("div", "compact-promo-actions"); + var compactDonate = element("a", "compact-promo-button compact-donate-button"); + compactDonate.href = donateLink.href; + compactDonate.target = "_blank"; + compactDonate.rel = "noopener noreferrer"; + compactDonate.title = "Support YACReader"; + compactDonate.setAttribute("aria-label", "Support YACReader with a donation"); + compactDonate.appendChild(svgIcon("compact-promo-icon", '')); + + var compactMobile = element("button", "compact-promo-button"); + compactMobile.type = "button"; + compactMobile.title = "Get the mobile apps"; + compactMobile.setAttribute("aria-label", "Get the YACReader mobile apps"); + compactMobile.setAttribute("aria-haspopup", "dialog"); + compactMobile.setAttribute("aria-expanded", "false"); + compactMobile.setAttribute("aria-controls", "compact-mobile-popover"); + compactMobile.appendChild(svgIcon("compact-promo-icon", '')); + + var compactPopover = element("div", "compact-mobile-popover"); + compactPopover.id = "compact-mobile-popover"; + compactPopover.hidden = true; + compactPopover.setAttribute("role", "dialog"); + compactPopover.setAttribute("aria-labelledby", "compact-mobile-popover-title"); + + var compactPopoverHeader = element("div", "compact-mobile-popover-header"); + var compactPopoverHeading = element("div"); + var compactPopoverTitle = element("h2", "", "Upgrade your reading"); + compactPopoverTitle.id = "compact-mobile-popover-title"; + compactPopoverHeading.appendChild(compactPopoverTitle); + + var compactPopoverClose = element("button", "compact-popover-close"); + compactPopoverClose.type = "button"; + compactPopoverClose.setAttribute("aria-label", "Close mobile apps"); + compactPopoverClose.appendChild(svgIcon("compact-popover-close-icon", '')); + compactPopoverHeader.append(compactPopoverHeading, compactPopoverClose); + + compactPopover.append( + compactPopoverHeader, + element("p", "", "Unlock a richer reading experience with panel-by-panel navigation, flexible fit modes, image filters, fluid animations, and much more."), + storeBadge( + "app-store-badge", + "/images/webui/app-store-badge.svg", + "https://apps.apple.com/app/id635717885", + "Download YACReader on the App Store" + ), + storeBadge( + "google-play-badge", + "/images/webui/google-play-badge.png", + "https://play.google.com/store/apps/details?id=com.yacreader.yacreader", + "Get YACReader on Google Play" + ) + ); + + function closeCompactPopover(returnFocus) { + if (compactPopover.hidden) { + return; + } + compactPopover.hidden = true; + compactMobile.setAttribute("aria-expanded", "false"); + if (returnFocus) { + compactMobile.focus(); + } + } + + compactMobile.addEventListener("click", function (event) { + event.stopPropagation(); + var willOpen = compactPopover.hidden; + if (willOpen) { + compactPopover.hidden = false; + compactMobile.setAttribute("aria-expanded", "true"); + } else { + closeCompactPopover(false); + } + }); + compactPopoverClose.addEventListener("click", function () { + closeCompactPopover(true); + }); + compactPopover.addEventListener("click", function (event) { + event.stopPropagation(); + }); + document.addEventListener("click", function () { + closeCompactPopover(false); + }); + document.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeCompactPopover(true); + } + }); + var compactLayout = window.matchMedia("(max-width: 800px)"); + if (compactLayout.addEventListener) { + compactLayout.addEventListener("change", function (event) { + if (!event.matches) { + closeCompactPopover(false); + } + }); + } + + compactActions.append(compactDonate, compactMobile, compactPopover); + sidebarHeader.insertBefore(compactActions, themeToggle); + } + function initLibraryBrowser() { var browserRoot = document.querySelector("[data-browser-root]"); if (!browserRoot) { @@ -49,6 +405,7 @@ var navigationVersion = 0; var folderMetadataCache = {}; var browserBackAction = null; + var readerCleanup = null; if (browserBack) { browserBack.addEventListener("click", function () { @@ -121,6 +478,10 @@ return libraryUrl() + "/comic/" + encodeURIComponent(comicId); } + function readerUrl(comicId) { + return comicUrl(comicId) + "/read"; + } + function folderContentApi(folderId) { return "/v2/library/" + encodeURIComponent(libraryId) + "/folder/" + encodeURIComponent(folderId) + "/content"; } @@ -133,6 +494,38 @@ return "/v2/library/" + encodeURIComponent(libraryId) + "/comic/" + encodeURIComponent(comicId) + "/fullinfo"; } + function comicOpenApi(comicId) { + return "/v2/library/" + encodeURIComponent(libraryId) + "/comic/" + encodeURIComponent(comicId) + "/remote"; + } + + function comicPageApi(comicId, page) { + return "/v2/library/" + encodeURIComponent(libraryId) + "/comic/" + encodeURIComponent(comicId) + "/page/" + page + "/remote"; + } + + function comicProgressApi(comicId) { + return "/v2/library/" + encodeURIComponent(libraryId) + "/comic/" + encodeURIComponent(comicId) + "/update"; + } + + function apiHeaders(accept) { + var headers = {}; + if (accept) { + headers.Accept = accept; + } + var id = requestId(); + if (id) { + headers["X-Request-Id"] = id; + } + return headers; + } + + function leaveReader() { + if (readerCleanup) { + readerCleanup(); + readerCleanup = null; + } + document.body.classList.remove("web-reader-active"); + } + function safeCoverPath(path) { return String(path || "") .split("/") @@ -414,6 +807,7 @@ } function showFolder(folderId, pushHistory) { + leaveReader(); var version = ++navigationVersion; showLoading(); @@ -500,10 +894,116 @@ return names[Number(fileType)] || ""; } - function detailField(label, value) { + function formatFileSize(value) { + var bytes = Number(value); + if (!Number.isFinite(bytes) || bytes <= 0) { + return ""; + } + + var units = ["B", "KB", "MB", "GB", "TB"]; + var unit = 0; + while (bytes >= 1024 && unit < units.length - 1) { + bytes /= 1024; + unit += 1; + } + + var precision = unit === 0 || bytes >= 100 ? 0 : 1; + return bytes.toFixed(precision) + " " + units[unit]; + } + + function numberedMetadata(name, number, count) { + if (!hasValue(name)) { + return ""; + } + if (hasValue(number) && hasValue(count)) { + return "(" + number + "/" + count + ") " + name; + } + if (hasValue(number)) { + return "(" + number + ") " + name; + } + return name; + } + + var comicVineBaseUrl = "http://www.comicvine.com"; + + function safeExternalUrl(value) { + var candidate = String(value || "").trim(); + if (!candidate) { + return ""; + } + if (/^www\./i.test(candidate)) { + candidate = "http://" + candidate; + } else if (candidate.startsWith("//")) { + candidate = "http:" + candidate; + } else if (!/^[a-z][a-z0-9+.-]*:/i.test(candidate) && !candidate.startsWith("//")) { + // Match ScraperScrollLabel::openLink(): Comic Vine descriptions store + // relative hrefs and the desktop UI prefixes them with this host. + candidate = comicVineBaseUrl + (candidate.startsWith("/") ? "" : "/") + candidate; + } + + try { + var url = new URL(candidate); + if (url.protocol === "http:" || url.protocol === "https:") { + return url.href; + } + } catch (error) { + } + + return ""; + } + + function externalLink(href, label) { + var url = safeExternalUrl(href); + if (!url) { + return null; + } + + var link = element("a", "", label); + link.href = url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + return link; + } + + function appendLinkifiedText(target, value) { + var text = String(value); + var urlPattern = /\b(?:https?:\/\/|www\.)[^\s<>"']+/gi; + var position = 0; + var match; + + while ((match = urlPattern.exec(text)) !== null) { + target.appendChild(document.createTextNode(text.slice(position, match.index))); + + var urlText = match[0]; + var trailing = ""; + var trailingMatch = urlText.match(/[.,;:!?\]\)}]+$/); + if (trailingMatch) { + trailing = trailingMatch[0]; + urlText = urlText.slice(0, -trailing.length); + } + + var link = externalLink(urlText, urlText); + target.appendChild(link || document.createTextNode(urlText)); + if (trailing) { + target.appendChild(document.createTextNode(trailing)); + } + position = match.index + match[0].length; + } + + target.appendChild(document.createTextNode(text.slice(position))); + } + + function detailField(label, value, href) { var field = element("div", "comic-metadata-field"); field.appendChild(element("dt", "", label)); - field.appendChild(element("dd", "", value)); + var detail = element("dd"); + var link = href ? externalLink(href, value) : null; + if (link) { + detail.appendChild(link); + } else { + appendLinkifiedText(detail, value); + } + field.appendChild(detail); return field; } @@ -556,14 +1056,19 @@ var result = document.createDocumentFragment(); if (source.body.children.length === 0) { - var plainText = element("p", "synopsis-plain", source.body.textContent); + var plainText = element("p", "synopsis-plain"); + appendLinkifiedText(plainText, source.body.textContent); result.appendChild(plainText); return result; } - function copyNode(node, target) { + function copyNode(node, target, linkifyText) { if (node.nodeType === Node.TEXT_NODE) { - target.appendChild(document.createTextNode(node.textContent)); + if (linkifyText) { + appendLinkifiedText(target, node.textContent); + } else { + target.appendChild(document.createTextNode(node.textContent)); + } return; } @@ -573,21 +1078,18 @@ if (!allowedElements[node.tagName]) { Array.from(node.childNodes).forEach(function (child) { - copyNode(child, target); + copyNode(child, target, linkifyText); }); return; } var clean = document.createElement(node.tagName.toLowerCase()); if (node.tagName === "A") { - try { - var url = new URL(node.getAttribute("href"), window.location.href); - if (url.protocol === "http:" || url.protocol === "https:") { - clean.href = url.href; - clean.target = "_blank"; - clean.rel = "noopener noreferrer"; - } - } catch (error) { + var url = safeExternalUrl(node.getAttribute("href")); + if (url) { + clean.href = url; + clean.target = "_blank"; + clean.rel = "noopener noreferrer"; } } else if (node.tagName === "TD" || node.tagName === "TH") { ["colspan", "rowspan"].forEach(function (attribute) { @@ -603,18 +1105,371 @@ } Array.from(node.childNodes).forEach(function (child) { - copyNode(child, clean); + copyNode(child, clean, linkifyText && node.tagName !== "A"); }); target.appendChild(clean); } Array.from(source.body.childNodes).forEach(function (node) { - copyNode(node, result); + copyNode(node, result, true); }); return result; } + function showReader(comicId, pushHistory, existingComic) { + leaveReader(); + var version = ++navigationVersion; + showLoading(); + + Promise.resolve(existingComic || fetchJson(comicInfoApi(comicId))).then(function (comic) { + if (version !== navigationVersion) { + return; + } + + var title = readableComicTitle(comic); + var numPages = Math.max(0, Number(comic.num_pages) || 0); + if (!numPages) { + throw new Error("This comic has no readable pages."); + } + + var savedPage = Number(comic.current_page) || 1; + var currentPage = Math.min(numPages - 1, Math.max(0, savedPage - 1)); + var requestedPage = currentPage; + var pageRequestVersion = 0; + var pageCache = Object.create(null); + var pageLoads = Object.create(null); + var pageAbortControllers = Object.create(null); + var destroyed = false; + var hasDisplayedPage = false; + var progressSynced = false; + var comicOpened = false; + + setPageHeading(title); + setBrowserBack(null); + browserRoot.removeAttribute("aria-busy"); + browserRoot.replaceChildren(); + document.body.classList.add("web-reader-active"); + + var reader = element("article", "web-reader"); + var toolbar = element("header", "web-reader-toolbar"); + var exitButton = element("button", "reader-toolbar-button reader-exit-button"); + exitButton.type = "button"; + exitButton.setAttribute("aria-label", "Back to comic details"); + exitButton.append(svgIcon("reader-toolbar-icon", ''), element("span", "", "Back")); + + var readerHeading = element("div", "web-reader-heading"); + readerHeading.append(element("div", "web-reader-title", title)); + var pageIndicator = element("div", "web-reader-page-indicator"); + pageIndicator.setAttribute("aria-live", "polite"); + toolbar.append(exitButton, readerHeading, pageIndicator); + + var stage = element("div", "web-reader-stage"); + var previousButton = element("button", "reader-page-button reader-previous-button"); + previousButton.type = "button"; + previousButton.setAttribute("aria-label", "Previous page"); + previousButton.appendChild(svgIcon("reader-page-button-icon", '')); + + var imageFrame = element("div", "web-reader-image-frame"); + var pageImage = element("img", "web-reader-page"); + pageImage.alt = ""; + pageImage.hidden = true; + var loading = element("div", "web-reader-loading"); + loading.setAttribute("role", "status"); + loading.append(element("span", "web-reader-spinner"), element("span", "", "Loading page…")); + var errorState = element("div", "web-reader-error"); + errorState.hidden = true; + errorState.append(element("strong", "", "Couldn’t load this page"), element("span", "", "The comic may still be opening on the server.")); + var retryButton = element("button", "secondary-button", "Try again"); + retryButton.type = "button"; + errorState.appendChild(retryButton); + imageFrame.append(pageImage, loading, errorState); + + var nextButton = element("button", "reader-page-button reader-next-button"); + nextButton.type = "button"; + nextButton.setAttribute("aria-label", "Next page"); + nextButton.appendChild(svgIcon("reader-page-button-icon", '')); + stage.append(previousButton, imageFrame, nextButton); + reader.append(toolbar, stage); + browserRoot.appendChild(reader); + showFirstReaderPromotion(exitButton); + + function updateNavigation() { + pageIndicator.textContent = (requestedPage + 1) + " / " + numPages; + previousButton.disabled = !comicOpened || requestedPage <= 0; + nextButton.disabled = !comicOpened || requestedPage >= numPages - 1; + } + + function syncProgress() { + if (!hasDisplayedPage || progressSynced) { + return; + } + progressSynced = true; + var headers = apiHeaders("text/plain"); + headers["Content-Type"] = "text/plain; charset=utf-8"; + fetch(comicProgressApi(comicId), { + method: "POST", + headers: headers, + body: "currentPage:" + (currentPage + 1) + "\n", + keepalive: true + }).catch(function () { + }); + } + + function cancelledPageError() { + var error = new Error("Page request cancelled."); + error.name = "AbortError"; + return error; + } + + function pageFetch(page, controller, attempt) { + if (destroyed || (controller && controller.signal.aborted)) { + return Promise.reject(cancelledPageError()); + } + return fetch(comicPageApi(comicId, page), { + headers: apiHeaders("image/jpeg"), + cache: "no-store", + signal: controller ? controller.signal : undefined + }).then(function (response) { + if (response.status === 412 && attempt < 120) { + return new Promise(function (resolve) { + window.setTimeout(resolve, 500); + }).then(function () { + return pageFetch(page, controller, attempt + 1); + }); + } + if (!response.ok) { + var error = new Error("Page request failed with status " + response.status); + error.status = response.status; + throw error; + } + return response.blob(); + }); + } + + function decodePage(blob) { + return new Promise(function (resolve, reject) { + var objectUrl = URL.createObjectURL(blob); + var decodedImage = new Image(); + decodedImage.onload = function () { + resolve(objectUrl); + }; + decodedImage.onerror = function () { + URL.revokeObjectURL(objectUrl); + reject(new Error("Page image could not be decoded.")); + }; + decodedImage.src = objectUrl; + }); + } + + function ensurePage(page) { + if (Object.prototype.hasOwnProperty.call(pageCache, page)) { + return Promise.resolve(pageCache[page]); + } + if (pageLoads[page]) { + return pageLoads[page]; + } + + var controller = window.AbortController ? new AbortController() : null; + pageAbortControllers[page] = controller; + pageLoads[page] = pageFetch(page, controller, 0).then(decodePage).then(function (objectUrl) { + delete pageLoads[page]; + delete pageAbortControllers[page]; + if (destroyed) { + URL.revokeObjectURL(objectUrl); + throw cancelledPageError(); + } + pageCache[page] = objectUrl; + return objectUrl; + }).catch(function (error) { + delete pageLoads[page]; + delete pageAbortControllers[page]; + throw error; + }); + return pageLoads[page]; + } + + function preloadAdjacentPages(page) { + [page - 1, page + 1].forEach(function (adjacentPage) { + if (adjacentPage >= 0 && adjacentPage < numPages) { + ensurePage(adjacentPage).catch(function () { + }); + } + }); + } + + function prunePageCache(page) { + Object.keys(pageCache).forEach(function (cachedPage) { + if (Math.abs(Number(cachedPage) - page) > 1) { + URL.revokeObjectURL(pageCache[cachedPage]); + delete pageCache[cachedPage]; + } + }); + Object.keys(pageAbortControllers).forEach(function (loadingPage) { + if (Math.abs(Number(loadingPage) - page) > 1 && pageAbortControllers[loadingPage]) { + pageAbortControllers[loadingPage].abort(); + } + }); + } + + function loadPage(page) { + if (destroyed) { + return; + } + requestedPage = Math.min(numPages - 1, Math.max(0, page)); + updateNavigation(); + errorState.hidden = true; + var pageWasCached = Object.prototype.hasOwnProperty.call(pageCache, requestedPage); + loading.hidden = pageWasCached; + imageFrame.classList.toggle("is-loading", !pageWasCached); + pageRequestVersion += 1; + var requestVersion = pageRequestVersion; + var pageToDisplay = requestedPage; + + ensurePage(pageToDisplay).then(function (objectUrl) { + if (destroyed || requestVersion !== pageRequestVersion) { + return; + } + pageImage.src = objectUrl; + pageImage.alt = title + ", page " + (pageToDisplay + 1); + pageImage.hidden = false; + loading.hidden = true; + imageFrame.classList.remove("is-loading"); + currentPage = pageToDisplay; + hasDisplayedPage = true; + prunePageCache(currentPage); + preloadAdjacentPages(currentPage); + }).catch(function (error) { + if (destroyed || requestVersion !== pageRequestVersion || (error && error.name === "AbortError")) { + return; + } + if (error && (error.status === 404 || error.status === 424)) { + comicOpened = false; + updateNavigation(); + } + loading.hidden = true; + imageFrame.classList.remove("is-loading"); + errorState.hidden = false; + }); + } + + function openComicAndLoad() { + errorState.hidden = true; + loading.hidden = false; + imageFrame.classList.add("is-loading"); + fetch(comicOpenApi(comicId), { + headers: apiHeaders("text/plain"), + cache: "no-store" + }).then(function (response) { + if (!response.ok) { + throw new Error("Comic request failed with status " + response.status); + } + if (destroyed) { + return; + } + comicOpened = true; + loadPage(requestedPage); + }).catch(function () { + if (destroyed) { + return; + } + comicOpened = false; + updateNavigation(); + loading.hidden = true; + imageFrame.classList.remove("is-loading"); + errorState.hidden = false; + }); + } + + function exitReader() { + if (history.state && history.state.view === "reader" && history.state.fromComicDetail) { + history.back(); + } else { + showComic(comicId, false); + } + } + + function handleReaderKeys(event) { + if (event.key === "ArrowLeft") { + event.preventDefault(); + if (requestedPage > 0) { + loadPage(requestedPage - 1); + } + } else if (event.key === "ArrowRight" || event.key === " ") { + event.preventDefault(); + if (requestedPage < numPages - 1) { + loadPage(requestedPage + 1); + } + } else if (event.key === "Escape") { + event.preventDefault(); + exitReader(); + } + } + + function handleReaderPageHide(event) { + if (!event.persisted) { + syncProgress(); + } + } + + previousButton.addEventListener("click", function () { + loadPage(requestedPage - 1); + }); + nextButton.addEventListener("click", function () { + loadPage(requestedPage + 1); + }); + retryButton.addEventListener("click", function () { + if (comicOpened) { + loadPage(requestedPage); + } else { + openComicAndLoad(); + } + }); + exitButton.addEventListener("click", exitReader); + document.addEventListener("keydown", handleReaderKeys); + window.addEventListener("pagehide", handleReaderPageHide); + + readerCleanup = function () { + syncProgress(); + destroyed = true; + pageRequestVersion += 1; + Object.keys(pageAbortControllers).forEach(function (page) { + if (pageAbortControllers[page]) { + pageAbortControllers[page].abort(); + } + }); + Object.keys(pageCache).forEach(function (page) { + URL.revokeObjectURL(pageCache[page]); + }); + document.removeEventListener("keydown", handleReaderKeys); + window.removeEventListener("pagehide", handleReaderPageHide); + }; + + var existingReaderState = !pushHistory && history.state && history.state.view === "reader" + ? Boolean(history.state.fromComicDetail) + : false; + var url = readerUrl(comicId); + var state = { view: "reader", itemId: comicId, fromComicDetail: pushHistory || existingReaderState }; + if (pushHistory) { + history.pushState(state, "", url); + } else { + history.replaceState(state, "", url); + } + + updateNavigation(); + openComicAndLoad(); + }).catch(function () { + if (version !== navigationVersion) { + return; + } + leaveReader(); + showError(function () { + showReader(comicId, false); + }); + }); + } + function showComic(comicId, pushHistory) { + leaveReader(); var version = ++navigationVersion; showLoading(); @@ -651,35 +1506,92 @@ } coverColumn.appendChild(cover); - var back = element("a", "secondary-button comic-back-button", "Back to folder"); - back.href = folderUrl(parentId); - back.addEventListener("click", function (event) { - event.preventDefault(); - showFolder(parentId, true); + var read = element("button", "secondary-button comic-read-button", "Read"); + read.type = "button"; + read.addEventListener("click", function () { + showReader(comicId, true, comic); + }); + coverColumn.appendChild(read); + + var compactMobileActions = element("div", "comic-mobile-compact-actions"); + var compactReadMobile = element("button", "primary-button comic-read-mobile-button"); + compactReadMobile.type = "button"; + compactReadMobile.setAttribute("aria-expanded", "false"); + compactReadMobile.setAttribute("aria-controls", "comic-mobile-popover"); + compactReadMobile.append( + svgIcon("comic-read-mobile-icon", ''), + element("span", "", "Read on mobile") + ); + + var compactComicPopover = element("div", "compact-mobile-popover comic-mobile-popover"); + compactComicPopover.id = "comic-mobile-popover"; + compactComicPopover.hidden = true; + compactComicPopover.setAttribute("role", "dialog"); + compactComicPopover.setAttribute("aria-labelledby", "comic-mobile-popover-title"); + + var compactComicPopoverHeader = element("div", "compact-mobile-popover-header"); + var compactComicPopoverHeading = element("div"); + var compactComicPopoverTitle = element("h2", "", "Upgrade your reading"); + compactComicPopoverTitle.id = "comic-mobile-popover-title"; + compactComicPopoverHeading.appendChild(compactComicPopoverTitle); + var compactComicPopoverClose = element("button", "compact-popover-close"); + compactComicPopoverClose.type = "button"; + compactComicPopoverClose.setAttribute("aria-label", "Close mobile apps"); + compactComicPopoverClose.appendChild(svgIcon("compact-popover-close-icon", '')); + compactComicPopoverHeader.append(compactComicPopoverHeading, compactComicPopoverClose); + + compactComicPopover.append( + compactComicPopoverHeader, + element("p", "", "Unlock a richer reading experience with panel-by-panel navigation, flexible fit modes, image filters, fluid animations, and much more."), + storeBadge( + "app-store-badge", + "/images/webui/app-store-badge.svg", + "https://apps.apple.com/app/id635717885", + "Download YACReader on the App Store" + ), + storeBadge( + "google-play-badge", + "/images/webui/google-play-badge.png", + "https://play.google.com/store/apps/details?id=com.yacreader.yacreader", + "Get YACReader on Google Play" + ) + ); + + function closeComicMobilePopover(returnFocus) { + compactComicPopover.hidden = true; + compactReadMobile.setAttribute("aria-expanded", "false"); + document.removeEventListener("pointerdown", handleComicMobileOutside); + if (returnFocus) { + compactReadMobile.focus(); + } + } + + function handleComicMobileOutside(event) { + if (!compactMobileActions.contains(event.target)) { + closeComicMobilePopover(false); + } + } + + compactReadMobile.addEventListener("click", function () { + if (compactComicPopover.hidden) { + compactComicPopover.hidden = false; + compactReadMobile.setAttribute("aria-expanded", "true"); + document.addEventListener("pointerdown", handleComicMobileOutside); + } else { + closeComicMobilePopover(false); + } + }); + compactComicPopoverClose.addEventListener("click", function () { + closeComicMobilePopover(true); + }); + compactMobileActions.addEventListener("keydown", function (event) { + if (event.key === "Escape") { + closeComicMobilePopover(true); + } }); - coverColumn.appendChild(back); - - var mobilePromo = element("aside", "mobile-app-promo"); - mobilePromo.appendChild(element("div", "section-title", "Read on mobile")); - mobilePromo.appendChild(element("h3", "", "Take your library with you")); - mobilePromo.appendChild(element("p", "", "Browse this library and read your comics with YACReader for iOS or Android.")); - - var mobileLinks = element("div", "mobile-app-links"); - var iosLink = element("a", "mobile-app-link", "iOS app"); - iosLink.href = "https://apps.apple.com/app/id635717885"; - iosLink.target = "_blank"; - iosLink.rel = "noopener noreferrer"; - iosLink.setAttribute("aria-label", "Get YACReader for iOS on the App Store"); - - var androidLink = element("a", "mobile-app-link", "Android app"); - androidLink.href = "https://play.google.com/store/apps/details?id=com.yacreader.yacreader"; - androidLink.target = "_blank"; - androidLink.rel = "noopener noreferrer"; - androidLink.setAttribute("aria-label", "Get YACReader for Android on Google Play"); - - mobileLinks.append(iosLink, androidLink); - mobilePromo.appendChild(mobileLinks); - coverColumn.appendChild(mobilePromo); + + compactMobileActions.append(compactReadMobile, compactComicPopover); + coverColumn.appendChild(compactMobileActions); var copy = element("div", "comic-detail-copy"); copy.appendChild(element("div", "section-title", "Comic information")); @@ -705,6 +1617,10 @@ if (fileType) { facts.appendChild(element("span", "comic-fact", fileType)); } + var fileSize = formatFileSize(comic.file_size); + if (fileSize) { + facts.appendChild(element("span", "comic-fact", fileSize)); + } copy.appendChild(facts); if (hasValue(comic.synopsis)) { @@ -718,13 +1634,14 @@ var metadata = [ ["Series", comic.series], - ["Issue", comic.universal_number], + ["Issue", hasValue(comic.universal_number) && hasValue(comic.count) ? comic.universal_number + " / " + comic.count : comic.universal_number], ["Volume", comic.volume], ["Publisher", comic.publisher], ["Imprint", comic.imprint], ["Date", comic.date], - ["Story arc", comic.story_arc], - ["Arc number", comic.arc_number], + ["Story arc", numberedMetadata(comic.story_arc, comic.arc_number, comic.arc_count)], + ["Alternate series", numberedMetadata(comic.alternate_series, comic.alternate_number, comic.alternate_count)], + ["Series group", comic.series_group], ["Genre", comic.genre], ["Writer", comic.writer], ["Penciller", comic.penciller], @@ -736,11 +1653,13 @@ ["Age rating", comic.age_rating], ["Language", comic.language_iso], ["Characters", comic.characters], + ["Main character or team", comic.main_character_or_team], ["Teams", comic.teams], ["Locations", comic.locations], ["Tags", comic.tags], ["Rating", Number(comic.rating) > 0 ? comic.rating + " / 5" : ""], - ["Color", comic.color === true ? "Color" : comic.color === false ? "Black and white" : ""] + ["Color", comic.color === true ? "Color" : comic.color === false ? "Black and white" : ""], + ["Comic Vine", hasValue(comic.comic_vine_id) ? "View on Comic Vine" : "", hasValue(comic.comic_vine_id) ? comicVineBaseUrl + "/comic/4000-" + encodeURIComponent(comic.comic_vine_id) + "/" : ""] ].filter(function (entry) { return hasValue(entry[1]); }); @@ -750,16 +1669,27 @@ metadataSection.appendChild(element("h3", "", "Metadata")); var metadataList = element("dl", "comic-metadata-grid"); metadata.forEach(function (entry) { - metadataList.appendChild(detailField(entry[0], entry[1])); + metadataList.appendChild(detailField(entry[0], entry[1], entry[2])); }); metadataSection.appendChild(metadataList); copy.appendChild(metadataSection); } + if (hasValue(comic.review)) { + var review = element("section", "comic-copy-section"); + review.appendChild(element("h3", "", "Review")); + var reviewContent = element("p"); + appendLinkifiedText(reviewContent, comic.review); + review.appendChild(reviewContent); + copy.appendChild(review); + } + if (hasValue(comic.notes)) { var notes = element("section", "comic-copy-section"); notes.appendChild(element("h3", "", "Notes")); - notes.appendChild(element("p", "", comic.notes)); + var notesContent = element("p"); + appendLinkifiedText(notesContent, comic.notes); + notes.appendChild(notesContent); copy.appendChild(notes); } @@ -785,19 +1715,21 @@ function routeFromLocation() { var escapedLibraryId = libraryId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - var match = window.location.pathname.match(new RegExp("^/webui/library/" + escapedLibraryId + "(?:/(folder|comic)/([0-9]+))?/?$")); + var match = window.location.pathname.match(new RegExp("^/webui/library/" + escapedLibraryId + "(?:/(folder|comic)/([0-9]+)(?:/(read))?)?/?$")); if (!match) { return { view: "folder", itemId: "1" }; } return { - view: match[1] || "folder", + view: match[3] ? "reader" : match[1] || "folder", itemId: match[2] || "1" }; } window.addEventListener("popstate", function () { var route = routeFromLocation(); - if (route.view === "comic") { + if (route.view === "reader") { + showReader(route.itemId, false); + } else if (route.view === "comic") { showComic(route.itemId, false); } else { showFolder(route.itemId, false); @@ -806,7 +1738,9 @@ var initialView = document.body.dataset.browserInitialView; var initialItemId = document.body.dataset.browserInitialItemId || "1"; - if (initialView === "comic") { + if (initialView === "reader") { + showReader(initialItemId, false); + } else if (initialView === "comic") { showComic(initialItemId, false); } else { showFolder(initialItemId, false); @@ -872,7 +1806,7 @@ var allButton = container.querySelector("[data-update-all]"); var cards = Array.prototype.slice.call(container.querySelectorAll("[data-library-card]")); - var triggers = Array.prototype.slice.call(container.querySelectorAll("[data-update-all], [data-update-library]")); + var triggers = Array.prototype.slice.call(container.querySelectorAll("[data-update-all], [data-update-library], [data-rescan-xml-library]")); if (!triggers.length) { return; } @@ -916,10 +1850,9 @@ window.setTimeout(poll, delay); } - // The server tracks a single global "running" flag and updates libraries - // sequentially, so it can't say which library is in progress. We show the - // indicator for whatever started the run (or fall back to "all" if a run was - // already going) and disable every trigger until it finishes. + // The server tracks a single global maintenance operation. We show the + // indicator for whatever started it (or fall back to "all" if it was already + // running) and disable every trigger until it finishes. function applyRunning(running) { if (running) { setTriggersDisabled(true); @@ -986,6 +1919,10 @@ if (libraryId) { var card = button.closest("[data-library-card]"); trigger(card, "/v2/library/" + encodeURIComponent(libraryId) + "/update"); + } else if (button.hasAttribute("data-rescan-xml-library")) { + var xmlLibraryId = button.getAttribute("data-rescan-xml-library"); + var xmlCard = button.closest("[data-library-card]"); + trigger(xmlCard, "/v2/library/" + encodeURIComponent(xmlLibraryId) + "/rescan-xml"); } else { trigger("all", "/v2/libraries/update"); } @@ -999,6 +1936,8 @@ applyTheme(preferredTheme()); document.addEventListener("DOMContentLoaded", function () { + initSidebarActions(); + var toggle = document.querySelector("[data-theme-toggle]"); if (toggle) { toggle.addEventListener("click", function () { diff --git a/shortcuts_management/shortcuts_manager.cpp b/shortcuts_management/shortcuts_manager.cpp index 87eca2b8f..f84f297cc 100644 --- a/shortcuts_management/shortcuts_manager.cpp +++ b/shortcuts_management/shortcuts_manager.cpp @@ -75,8 +75,8 @@ void ShortcutsManager::initDefaultShorcuts() defaultShorcuts.insert(OFFSET_DOUBLE_PAGE_TO_THE_LEFT_Y, Qt::CTRL | Qt::SHIFT | Qt::Key_Left); defaultShorcuts.insert(OFFSET_DOUBLE_PAGE_TO_THE_RIGHT_Y, Qt::CTRL | Qt::SHIFT | Qt::Key_Right); // mglass - defaultShorcuts.insert(SIZE_UP_MGLASS_ACTION_Y, Qt::Key_Plus); - defaultShorcuts.insert(SIZE_DOWN_MGLASS_ACTION_Y, Qt::Key_Minus); + defaultShorcuts.insert(SIZE_UP_MGLASS_ACTION_Y, Qt::Key_BracketRight); + defaultShorcuts.insert(SIZE_DOWN_MGLASS_ACTION_Y, Qt::Key_BracketLeft); defaultShorcuts.insert(ZOOM_IN_MGLASS_ACTION_Y, Qt::Key_Asterisk); defaultShorcuts.insert(ZOOM_OUT_MGLASS_ACTION_Y, Qt::Key_Underscore); defaultShorcuts.insert(RESET_MGLASS_ACTION_Y, Qt::Key_Slash); diff --git a/shortcuts_management/shortcuts_manager.h b/shortcuts_management/shortcuts_manager.h index 50175afa4..865f2a08d 100644 --- a/shortcuts_management/shortcuts_manager.h +++ b/shortcuts_management/shortcuts_manager.h @@ -43,9 +43,13 @@ class ShortcutsManager #define EXPORT_LIBRARY_ACTION_YL "EXPORT_LIBRARY_ACTION_YL" #define IMPORT_LIBRARY_ACTION_YL "IMPORT_LIBRARY_ACTION_YL" #define UPDATE_LIBRARY_ACTION_YL "UPDATE_LIBRARY_ACTION_YL" +#define BACKUP_LIBRARY_ACTION_YL "BACKUP_LIBRARY_ACTION_YL" +#define RESTORE_LIBRARY_ACTION_YL "RESTORE_LIBRARY_ACTION_YL" +#define REPAIR_LIBRARY_ACTION_YL "REPAIR_LIBRARY_ACTION_YL" #define RENAME_LIBRARY_ACTION_YL "RENAME_LIBRARY_ACTION_YL" #define REMOVE_LIBRARY_ACTION_YL "REMOVE_LIBRARY_ACTION_YL" #define RESCAN_LIBRARY_XML_INFO_ACTION_YL "RESCAN_LIBRARY_XML_INFO_ACTION_YL" +#define OPEN_LIBRARY_FOLDER_ACTION_YL "OPEN_LIBRARY_FOLDER_ACTION_YL" #define SHOW_LIBRARY_INFO_ACTION_YL "SHOW_LIBRARY_INFO_ACTION_YL" #define OPEN_COMIC_ACTION_YL "OPEN_COMIC_ACTION_YL" #define SET_AS_READ_ACTION_YL "SET_AS_READ_ACTION_YL" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 049d8b5c3..3ac52d8fe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(concurrent_queue_test) add_subdirectory(compressed_archive_test) add_subdirectory(continuous_view_model_test) add_subdirectory(pdf_render_size_test) +add_subdirectory(epub_page_index_test) diff --git a/tests/epub_page_index_test/CMakeLists.txt b/tests/epub_page_index_test/CMakeLists.txt new file mode 100644 index 000000000..721fc8999 --- /dev/null +++ b/tests/epub_page_index_test/CMakeLists.txt @@ -0,0 +1,12 @@ +# EPUB page index test + +qt_add_executable(epub_page_index_test + main.cpp +) +yacreader_apply_build_options(epub_page_index_test) +target_link_libraries(epub_page_index_test PRIVATE + Qt6::Core + Qt6::Test + epub_page_index +) +add_test(NAME epub_page_index_test COMMAND epub_page_index_test) diff --git a/tests/epub_page_index_test/main.cpp b/tests/epub_page_index_test/main.cpp new file mode 100644 index 000000000..6d5b0684e --- /dev/null +++ b/tests/epub_page_index_test/main.cpp @@ -0,0 +1,218 @@ +#include "epub_page_index.h" + +#include +#include +#include +#include +#include + +class EpubPageIndexTest : public QObject +{ + Q_OBJECT + +private slots: + void followsSpineOrderAndMetadata(); + void resolvesRelativeAndEncodedPaths(); + void readsObjectWrapper(); + void toleratesHtmlNamedEntities(); + void preservesDuplicateSpineReferences(); + void skipsBrokenSpineItems(); + void rejectsWrappersWithMultipleImages(); + void rejectsPathsOutsideTheArchive(); +}; + +namespace { + +YACReaderEpub::PageIndex readIndex(const QStringList &fileNames, const QHash &files) +{ + return YACReaderEpub::readPageIndex(fileNames, [&](int index) { return files.value(fileNames.at(index)); }); +} + +QByteArray containerXml() +{ + return R"( + + + + + )"; +} + +} + +void EpubPageIndexTest::followsSpineOrderAndMetadata() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + pre-paginated + + + + + + + + + + + + + )" }, + { "OEBPS/pages/a.xhtml", R"()" }, + { "OEBPS/pages/b.xhtml", R"()" }, + { "OEBPS/pages/notes.xhtml", R"()" }, + { "OEBPS/images/a.jpg", "a" }, + { "OEBPS/images/z.jpg", "z" }, + { "OEBPS/images/notes.jpg", "notes" }, + { "OEBPS/images/cover.jpg", "cover" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.size(), 2); + QCOMPARE(result.pages.at(0).fileName, QString("OEBPS/images/z.jpg")); + QCOMPARE(result.pages.at(1).fileName, QString("OEBPS/images/a.jpg")); + QVERIFY(result.fixedLayout); + QCOMPARE(result.coverPath, QString("OEBPS/images/cover.jpg")); +} + +void EpubPageIndexTest::resolvesRelativeAndEncodedPaths() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "OEBPS/pages/page.svg", R"( + + )" }, + { "OEBPS/images/Page 01.jpg", "page" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.constFirst().fileName, QString("OEBPS/images/Page 01.jpg")); +} + +void EpubPageIndexTest::readsObjectWrapper() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "OEBPS/page.xhtml", R"()" }, + { "OEBPS/images/page.jpg", "page" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.constFirst().fileName, QString("OEBPS/images/page.jpg")); +} + +void EpubPageIndexTest::toleratesHtmlNamedEntities() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "OEBPS/page.xhtml", R"(

 

)" }, + { "OEBPS/images/page.jpg", "page" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.constFirst().fileName, QString("OEBPS/images/page.jpg")); +} + +void EpubPageIndexTest::preservesDuplicateSpineReferences() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "OEBPS/images/page.jpg", "page" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.size(), 2); + QCOMPARE(result.pages.at(0).archiveIndex, result.pages.at(1).archiveIndex); +} + +void EpubPageIndexTest::skipsBrokenSpineItems() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + + )" }, + { "OEBPS/broken.xhtml", R"()" }, + { "OEBPS/valid.xhtml", R"()" }, + { "OEBPS/one.jpg", "one" }, + { "OEBPS/two.jpg", "two" }, + { "OEBPS/valid.jpg", "valid" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY2(result.isValid(), qPrintable(result.error)); + QCOMPARE(result.pages.size(), 1); + QCOMPARE(result.pages.constFirst().fileName, QString("OEBPS/valid.jpg")); +} + +void EpubPageIndexTest::rejectsWrappersWithMultipleImages() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "OEBPS/page.xhtml", R"()" }, + { "OEBPS/one.jpg", "one" }, + { "OEBPS/two.jpg", "two" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY(!result.isValid()); + QVERIFY(result.error.contains("no usable image pages")); +} + +void EpubPageIndexTest::rejectsPathsOutsideTheArchive() +{ + const QHash files { + { "META-INF/container.xml", containerXml() }, + { "OEBPS/content.opf", R"( + + )" }, + { "page.jpg", "page" }, + }; + const QStringList fileNames = files.keys(); + + const auto result = readIndex(fileNames, files); + + QVERIFY(!result.isValid()); + QVERIFY(result.error.contains("no usable image pages")); +} + +QTEST_GUILESS_MAIN(EpubPageIndexTest) + +#include "main.moc"