From 23559d8b0a706567417d558e3613d3d85fa45632 Mon Sep 17 00:00:00 2001 From: acx10 <8075870+acx10@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:24:35 -0600 Subject: [PATCH 1/5] Remove all traces of telemetry (#3366) Co-authored-by: acx10 --- .github/pull_request_template.md | 4 +- .github/release-drafter.yml | 5 +- .github/workflows/develop-pipeline.yml | 14 +- .github/workflows/master-pipeline.yml | 16 +- CONTRIBUTING.md | 16 +- Dockerfile | 4 +- Dockerfile.ci | 4 +- README.md | 123 +----- .../org/booklore/config/AppProperties.java | 7 - .../controller/ReadingSessionController.java | 2 +- .../java/org/booklore/crons/CronService.java | 148 ------- .../booklore/model/dto/BookloreTelemetry.java | 113 ----- .../booklore/model/dto/InstallationPing.java | 21 - .../model/dto/settings/AppSettingKey.java | 3 +- .../model/dto/settings/AppSettings.java | 3 +- .../booklore/service/TelemetryService.java | 182 -------- .../org/booklore/service/VersionService.java | 4 +- .../appsettings/AppSettingService.java | 1 - .../service/kobo/KepubConversionService.java | 2 +- .../metadata/parser/ComicvineBookParser.java | 2 +- .../metadata/parser/RanobeDbParser.java | 4 +- .../service/reader/FfprobeService.java | 2 +- .../java/org/booklore/util/FileService.java | 2 +- .../src/main/resources/application.yaml | 4 +- .../org/booklore/crons/CronServiceTest.java | 389 ------------------ booklore-ui/package-lock.json | 120 +++--- booklore-ui/package.json | 2 +- .../global-preferences.component.html | 27 -- .../global-preferences.component.ts | 9 +- .../external-doc-link.component.ts | 5 +- .../github-support-dialog.html | 2 +- .../layout-menu/app.menu.component.ts | 4 +- .../app/shared/model/app-settings.model.ts | 2 - .../src/i18n/da/settings-application.json | 93 ++--- .../src/i18n/de/settings-application.json | 95 ++--- .../src/i18n/en/settings-application.json | 5 - .../src/i18n/es/settings-application.json | 93 ++--- .../src/i18n/fr/settings-application.json | 91 ++-- .../src/i18n/hr/settings-application.json | 95 ++--- .../src/i18n/hu/settings-application.json | 91 ++-- .../src/i18n/id/settings-application.json | 91 ++-- .../src/i18n/it/settings-application.json | 91 ++-- .../src/i18n/ja/settings-application.json | 91 ++-- .../src/i18n/nl/settings-application.json | 91 ++-- .../src/i18n/pl/settings-application.json | 91 ++-- .../src/i18n/pt/settings-application.json | 91 ++-- .../src/i18n/ru/settings-application.json | 91 ++-- .../src/i18n/sk/settings-application.json | 91 ++-- .../src/i18n/sl/settings-application.json | 95 ++--- .../src/i18n/sv/settings-application.json | 95 ++--- .../src/i18n/uk/settings-application.json | 95 ++--- .../src/i18n/zh/settings-application.json | 91 ++-- docs/OIDC-Setup-With-PocketID.md | 2 +- docs/forward-auth-with-proxy.md | 2 +- example-chart/Chart.yaml | 2 +- example-chart/values.yaml | 2 +- example-docker/docker-compose.yml | 7 +- example-podman/README.md | 2 +- example-podman/booklore.container | 2 +- scripts/docker-buildx-push.sh | 7 +- scripts/notify-discord.sh | 9 +- scripts/weblate-setup.sh | 6 +- 62 files changed, 912 insertions(+), 2042 deletions(-) delete mode 100644 booklore-api/src/main/java/org/booklore/crons/CronService.java delete mode 100644 booklore-api/src/main/java/org/booklore/model/dto/BookloreTelemetry.java delete mode 100644 booklore-api/src/main/java/org/booklore/model/dto/InstallationPing.java delete mode 100644 booklore-api/src/main/java/org/booklore/service/TelemetryService.java delete mode 100644 booklore-api/src/test/java/org/booklore/crons/CronServiceTest.java diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 42666b67d..f5d1ed3f3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ ## ๐Ÿ“ Description **Linked Issue:** Fixes # -> **Required.** Every PR must reference an approved issue. If no issue exists, [open one](https://github.com/booklore-app/booklore/issues/new) and wait for maintainer approval before submitting a PR. Unsolicited PRs without a linked issue will be closed. +> **Required.** Every PR must reference an approved issue. If no issue exists, [open one](https://github.com/the-booklore/booklore/issues/new) and wait for maintainer approval before submitting a PR. Unsolicited PRs without a linked issue will be closed. ## ๐Ÿท๏ธ Type of Change @@ -82,7 +82,7 @@ ## โœ… Pre-Submission Checklist - [ ] PR is reasonably scoped (PRs over 1000+ changed lines will be closed, split into smaller PRs) - [ ] No unsolicited refactors, cleanups, or "improvements" are bundled in - [ ] Flyway migration versioning is correct _(if schema was modified)_ -- [ ] Documentation PR submitted to [booklore-docs](https://github.com/booklore-app/booklore-docs) _(if user-facing changes)_ +- [ ] Documentation PR submitted to [booklore-docs](https://github.com/the-booklore/booklore-docs) _(if user-facing changes)_ ### ๐Ÿค– AI-Assisted Contributions diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 89c5a28ab..92817fcda 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -66,7 +66,6 @@ template: | ## ๐Ÿณ Docker Images - - **Docker Hub:** `booklore/booklore:v$RESOLVED_VERSION` - - **GitHub Container Registry:** `ghcr.io/booklore-app/booklore:v$RESOLVED_VERSION` + - **GitHub Container Registry:** `ghcr.io/the-booklore/booklore:v$RESOLVED_VERSION` - **Full Changelog**: https://github.com/booklore-app/booklore/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION + **Full Changelog**: https://github.com/the-booklore/booklore/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index a6ccc9fb3..f4b684971 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -230,13 +230,6 @@ jobs: # ---------------------------------------- # Docker login (pushes & internal PRs only) # ---------------------------------------- - - name: Authenticate to Docker Hub - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Authenticate to GitHub Container Registry if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 @@ -257,14 +250,13 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - booklore/booklore:${{ env.image_tag }} - ghcr.io/booklore-app/booklore:${{ env.image_tag }} + ghcr.io/the-booklore/booklore:${{ env.image_tag }} build-args: | APP_VERSION=${{ env.image_tag }} APP_REVISION=${{ github.sha }} cache-from: | type=gha - type=registry,ref=ghcr.io/booklore-app/booklore:buildcache + type=registry,ref=ghcr.io/the-booklore/booklore:buildcache cache-to: | type=gha,mode=max - type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max + type=registry,ref=ghcr.io/the-booklore/booklore:buildcache,mode=max diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index 90ecb2293..ef2399f51 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -161,12 +161,6 @@ jobs: with: fetch-depth: 0 - - name: Authenticate to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Authenticate to GitHub Container Registry uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: @@ -306,19 +300,17 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - booklore/booklore:${{ env.new_tag }} - booklore/booklore:latest - ghcr.io/booklore-app/booklore:${{ env.new_tag }} - ghcr.io/booklore-app/booklore:latest + ghcr.io/the-booklore/booklore:${{ env.new_tag }} + ghcr.io/the-booklore/booklore:latest build-args: | APP_VERSION=${{ env.new_tag }} APP_REVISION=${{ github.sha }} cache-from: | type=gha - type=registry,ref=ghcr.io/booklore-app/booklore:buildcache + type=registry,ref=ghcr.io/the-booklore/booklore:buildcache cache-to: | type=gha,mode=max - type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max + type=registry,ref=ghcr.io/the-booklore/booklore:buildcache,mode=max - name: Update GitHub Release Draft uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ad06fe11..901d9cf13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ ## Table of Contents ## Before You Start -> **Issue first, PR second.** Every pull request must be linked to an approved issue. If you want to work on something, [open an issue](https://github.com/booklore-app/booklore/issues/new) (or find an existing one) and wait for a maintainer to approve it before writing code. PRs submitted without a linked, approved issue will be closed. +> **Issue first, PR second.** Every pull request must be linked to an approved issue. If you want to work on something, [open an issue](https://github.com/the-booklore/booklore/issues/new) (or find an existing one) and wait for a maintainer to approve it before writing code. PRs submitted without a linked, approved issue will be closed. This protects both your time and ours. It ensures that the work is actually wanted and that you're heading in the right direction before you invest effort. @@ -50,8 +50,8 @@ ## Where to Start Not sure where to begin? Look for issues labeled: -- [`good first issue`](https://github.com/booklore-app/booklore/labels/good%20first%20issue) - small, well-scoped tasks ideal for newcomers -- [`help wanted`](https://github.com/booklore-app/booklore/labels/help%20wanted) - tasks where maintainers would appreciate a hand +- [`good first issue`](https://github.com/the-booklore/booklore/labels/good%20first%20issue) - small, well-scoped tasks ideal for newcomers +- [`help wanted`](https://github.com/the-booklore/booklore/labels/help%20wanted) - tasks where maintainers would appreciate a hand --- @@ -59,12 +59,12 @@ ## Getting Started ### Fork and Clone -First, [fork the repository](https://github.com/booklore-app/booklore/fork) on GitHub, then clone your fork locally: +First, [fork the repository](https://github.com/the-booklore/booklore/fork) on GitHub, then clone your fork locally: ```bash git clone https://github.com//booklore.git cd booklore -git remote add upstream https://github.com/booklore-app/booklore.git +git remote add upstream https://github.com/the-booklore/booklore.git ``` ### Keep Your Fork in Sync @@ -274,7 +274,7 @@ ## Submitting a Pull Request - [ ] PR contains a single logical change (one bug fix OR one feature) - [ ] No unrelated refactors, style changes, or "improvements" are bundled in - [ ] **PR is reasonably sized.** PRs with 1000+ changed lines will be closed without review. Break large changes into small, focused PRs. -- [ ] **For user-facing features:** submit a companion docs PR at [booklore-docs](https://github.com/booklore-app/booklore-docs) +- [ ] **For user-facing features:** submit a companion docs PR at [booklore-docs](https://github.com/the-booklore/booklore-docs) > When you open your PR on GitHub, a **PR template** will appear. Fill it out completely, including test output and screenshots. @@ -326,7 +326,7 @@ ## Frontend Conventions ## Reporting Bugs -1. **Search [existing issues](https://github.com/booklore-app/booklore/issues)** to avoid duplicates. +1. **Search [existing issues](https://github.com/the-booklore/booklore/issues)** to avoid duplicates. 2. **Open a new issue** with the `bug` label including: - Clear, descriptive title (e.g., "Book import fails with PDF files over 100MB") - Steps to reproduce @@ -356,7 +356,7 @@ ## Reporting Bugs ## Community & Support - **Discord:** [Join the server](https://discord.gg/Ee5hd458Uz) for questions and discussion -- **GitHub Issues:** [Report bugs or request features](https://github.com/booklore-app/booklore/issues) +- **GitHub Issues:** [Report bugs or request features](https://github.com/the-booklore/booklore/issues) --- diff --git a/Dockerfile b/Dockerfile index d5e2eba59..77184887c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,8 +46,8 @@ ARG APP_REVISION # Set OCI labels LABEL org.opencontainers.image.title="BookLore" \ org.opencontainers.image.description="BookLore: A self-hosted, multi-user digital library with smart shelves, auto metadata, Kobo & KOReader sync, BookDrop imports, OPDS support, and a built-in reader for EPUB, PDF, and comics." \ - org.opencontainers.image.source="https://github.com/booklore-app/booklore" \ - org.opencontainers.image.url="https://github.com/booklore-app/booklore" \ + org.opencontainers.image.source="https://github.com/the-booklore/booklore" \ + org.opencontainers.image.url="https://github.com/the-booklore/booklore" \ org.opencontainers.image.documentation="https://booklore.org/docs/getting-started" \ org.opencontainers.image.version=$APP_VERSION \ org.opencontainers.image.revision=$APP_REVISION \ diff --git a/Dockerfile.ci b/Dockerfile.ci index b4dc8c736..835e50c5f 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -8,8 +8,8 @@ ARG APP_REVISION LABEL org.opencontainers.image.title="BookLore" \ org.opencontainers.image.description="BookLore: A self-hosted, multi-user digital library with smart shelves, auto metadata, Kobo & KOReader sync, BookDrop imports, OPDS support, and a built-in reader for EPUB, PDF, and comics." \ - org.opencontainers.image.source="https://github.com/booklore-app/booklore" \ - org.opencontainers.image.url="https://github.com/booklore-app/booklore" \ + org.opencontainers.image.source="https://github.com/the-booklore/booklore" \ + org.opencontainers.image.url="https://github.com/the-booklore/booklore" \ org.opencontainers.image.documentation="https://booklore.org/docs/getting-started" \ org.opencontainers.image.version=$APP_VERSION \ org.opencontainers.image.revision=$APP_REVISION \ diff --git a/README.md b/README.md index 70f7c0e6d..3902d0b2f 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,6 @@ Organize, read, annotate, sync across devices, and share, all without relying on third-party services.

-

- Release - License - Docker Pulls - Stars - Discord - Open Collective - Translate -

- -

- ๐ŸŒ Website ยท - ๐Ÿ“– Docs ยท - ๐ŸŽฎ Demo ยท - ๐Ÿš€ Quick Start ยท - ๐Ÿ’ฌ Discord -

- -

- BookLore Demo -

- --- ## โœจ Features @@ -53,20 +31,14 @@ ## โœจ Features ## ๐Ÿš€ Quick Start -> [!TIP] -> Looking for OIDC setup, advanced config, or upgrade guides? See the [full documentation](https://booklore.org/docs/getting-started). - All you need is [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/).
๐Ÿ“ฆ Image Repositories -| Registry | Image | -|----------|-------| -| Docker Hub | `booklore/booklore` | -| GitHub Container Registry | `ghcr.io/booklore-app/booklore` | - -> Legacy images at `ghcr.io/adityachandelgit/booklore-app` remain available but won't receive updates. +| Registry | Image | +|----------|------------------------------------| +| GitHub Container Registry | `ghcr.io/the-booklore/booklore` |
@@ -102,8 +74,7 @@ ### Step 2: Docker Compose ```yaml services: booklore: - image: booklore/booklore:latest - # Alternative: ghcr.io/booklore-app/booklore:latest + image: ghcr.io/the-booklore/booklore:latest container_name: booklore environment: - USER_ID=${APP_USER_ID} @@ -170,21 +141,6 @@ ## โš ๏ธ Network Storage (NAS / NFS / SMB / CIFS) --- -## ๐ŸŽฎ Live Demo - -See BookLore in action before deploying your own instance. - -| | | -|:---|:---| -| ๐ŸŒ **URL** | **[demo.booklore.org](https://demo.booklore.org)** | -| ๐Ÿ‘ค **Username** | `booklore` | -| ๐Ÿ”‘ **Password** | `9HC20PGGfitvWaZ1` | - -> [!NOTE] -> This is a standard user account. Admin features like library creation, user management, and system settings are only available on your own instance. - ---- - ## ๐Ÿ“ฅ BookDrop: Zero-Effort Import Drop book files into a folder. BookLore picks them up, pulls metadata, and queues everything for your review. @@ -212,20 +168,6 @@ ## ๐Ÿ“ฅ BookDrop: Zero-Effort Import --- -## ๐Ÿค Community & Support - -| | | -|:---|:---| -| ๐Ÿž **Something not working?** | [Report a Bug](https://github.com/booklore-app/booklore/issues/new?template=bug_report.yml) | -| ๐Ÿ’ก **Got an idea?** | [Request a Feature](https://github.com/booklore-app/booklore/issues/new?template=feature_request.yml) | -| ๐Ÿ› ๏ธ **Want to help build?** | [Contributing Guide](CONTRIBUTING.md) | -| ๐Ÿ’ฌ **Come hang out** | [Discord Server](https://discord.gg/Ee5hd458Uz) | - -> [!WARNING] -> **Before opening a PR:** Open an issue first and get maintainer approval. PRs without a linked issue, without screenshots/video proof, or without pasted test output will be closed. All code must follow project [backend](CONTRIBUTING.md#backend-conventions) and [frontend](CONTRIBUTING.md#frontend-conventions) conventions. AI-assisted contributions are welcome, but you must run, test, and understand every line you submit. See the [Contributing Guide](CONTRIBUTING.md) for full details. - ---- - ## ๐Ÿ’œ Support BookLore BookLore is free, open source, and built with care. Here's how you can give back: @@ -236,10 +178,6 @@ ## ๐Ÿ’œ Support BookLore | ๐Ÿ’ฐ **Sponsor development** | [Open Collective](https://opencollective.com/booklore) funds hosting, testing, and new features | | ๐Ÿ“ข **Tell someone** | Share BookLore with a friend, a subreddit, or your local book club | -> [!IMPORTANT] -> We're raising funds for a Kobo device to build and test native Kobo sync support. -> [Contribute to the Kobo Bounty โ†’](https://opencollective.com/booklore/projects/kobo-device-for-testing) - --- ## ๐ŸŒ Translations @@ -252,76 +190,23 @@ ## ๐ŸŒ Translations --- -## ๐Ÿ“Š Project Analytics - -![Repository Activity](https://repobeats.axiom.co/api/embed/44a04220bfc5136e7064181feb07d5bf0e59e27e.svg) - -### โญ Star History - - - - - - Star History Chart - - - ---- - -## ๐Ÿ‘ฅ Contributors - -[![Contributors](https://contrib.rocks/image?repo=adityachandelgit/BookLore)](https://github.com/booklore-app/booklore/graphs/contributors) - -Every contribution matters. [See how you can help โ†’](CONTRIBUTING.md) - ---- - -
- ## ๐ŸŒŸ Sponsors & Partners - - -
- - - Run on PikaPods - - -**PikaPods** - - - - - ElfHosted - - -**ElfHosted** - - JetBrains -**JetBrains**
- -*Want your logo here? [Become a sponsor โ†’](https://opencollective.com/booklore)* -
--- -## โš ๏ธ Note to Integrators - -While BookLore is open source and its API is accessible, it is not designed or maintained as a stable integration point. Endpoints are undocumented, unversioned, and may change or break at any time without notice. No compatibility guarantees or support are provided for third-party use. -
## โš–๏ธ License diff --git a/booklore-api/src/main/java/org/booklore/config/AppProperties.java b/booklore-api/src/main/java/org/booklore/config/AppProperties.java index 0929c1229..629b97199 100644 --- a/booklore-api/src/main/java/org/booklore/config/AppProperties.java +++ b/booklore-api/src/main/java/org/booklore/config/AppProperties.java @@ -15,7 +15,6 @@ public class AppProperties { private String version; private RemoteAuth remoteAuth; private Boolean forceDisableOidc = false; - private Telemetry telemetry = new Telemetry(); /** * Type of disk storage where library files are stored. @@ -41,10 +40,4 @@ public class AppProperties { private String adminGroup; private String groupsDelimiter = "\\s+"; // Default to whitespace for backward compatibility } - - @Getter - @Setter - public static class Telemetry { - private String baseUrl = "https://telemetry.booklore.org"; - } } diff --git a/booklore-api/src/main/java/org/booklore/controller/ReadingSessionController.java b/booklore-api/src/main/java/org/booklore/controller/ReadingSessionController.java index a51f54f76..a0ecb0602 100644 --- a/booklore-api/src/main/java/org/booklore/controller/ReadingSessionController.java +++ b/booklore-api/src/main/java/org/booklore/controller/ReadingSessionController.java @@ -23,7 +23,7 @@ public class ReadingSessionController { private final ReadingSessionService readingSessionService; - @Operation(summary = "Record a reading session", description = "Receive telemetry from the reader client and persist or log the session.") + @Operation(summary = "Record a reading session", description = "Receive reading session data from the reader client and persist or log the session.") @ApiResponses({ @ApiResponse(responseCode = "202", description = "Reading session accepted"), @ApiResponse(responseCode = "400", description = "Invalid payload") diff --git a/booklore-api/src/main/java/org/booklore/crons/CronService.java b/booklore-api/src/main/java/org/booklore/crons/CronService.java deleted file mode 100644 index dcf244ed5..000000000 --- a/booklore-api/src/main/java/org/booklore/crons/CronService.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.booklore.crons; - -import jakarta.annotation.PostConstruct; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.booklore.config.AppProperties; -import org.booklore.model.dto.BookloreTelemetry; -import org.booklore.model.dto.InstallationPing; -import org.booklore.model.dto.settings.AppSettings; -import org.booklore.service.TelemetryService; -import org.booklore.service.appsettings.AppSettingService; -import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestClient; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.TimeUnit; - -@Service -@AllArgsConstructor -@DependsOnDatabaseInitialization -@Slf4j -public class CronService { - - private static final String LAST_TELEMETRY_KEY = "last_telemetry_sent"; - private static final String LAST_PING_KEY = "last_ping_sent"; - private static final String LAST_PING_APP_VERSION_KEY = "last_ping_app_version"; - private static final long INTERVAL_HOURS = 24; - - private final AppProperties appProperties; - private final TelemetryService telemetryService; - private final RestClient restClient; - private final AppSettingService appSettingService; - - @PostConstruct - public void initScheduledTasks() { - checkAndRunTelemetry(); - checkAndRunPing(); - } - - @Scheduled(fixedDelay = 24, timeUnit = TimeUnit.HOURS, initialDelay = 24) - public void sendTelemetryData() { - if (isTelemetryEnabled()) { - String url = appProperties.getTelemetry().getBaseUrl() + "/api/v1/ingest"; - BookloreTelemetry telemetry = telemetryService.collectTelemetry(); - if (postData(url, telemetry)) { - appSettingService.saveSetting(LAST_TELEMETRY_KEY, Instant.now().toString()); - } - } - } - - @Scheduled(fixedDelay = 24, timeUnit = TimeUnit.HOURS, initialDelay = 12) - public void sendPing() { - if (isTelemetryEnabled()) { - String url = appProperties.getTelemetry().getBaseUrl() + "/api/v1/heartbeat"; - InstallationPing ping = telemetryService.getInstallationPing(); - if (ping != null && postData(url, ping)) { - appSettingService.saveSetting(LAST_PING_KEY, Instant.now().toString()); - appSettingService.saveSetting(LAST_PING_APP_VERSION_KEY, ping.getAppVersion()); - } - } - } - - protected boolean postData(String url, Object body) { - try { - restClient.post() - .uri(url) - .body(body) - .retrieve() - .body(String.class); - return true; - } catch (Exception ex) { - log.debug("POST request to URL: {}, Message: {}", url, ex.getMessage()); - return false; - } - } - - private boolean isTelemetryEnabled() { - AppSettings settings = appSettingService.getAppSettings(); - return settings != null && settings.isTelemetryEnabled(); - } - - private void checkAndRunTelemetry() { - AppSettings settings = appSettingService.getAppSettings(); - if (settings == null || !settings.isTelemetryEnabled()) { - return; - } - String lastRunStr = appSettingService.getSettingValue(LAST_TELEMETRY_KEY); - if (shouldRunTask(lastRunStr)) { - log.info("Running stats on startup (last run: {})", lastRunStr); - sendTelemetryData(); - } - } - - private void checkAndRunPing() { - String lastRunStr = appSettingService.getSettingValue(LAST_PING_KEY); - if (hasAppVersionChanged()) { - log.info("App version changed, sending immediate ping"); - sendPing(); - return; - } - if (shouldRunTask(lastRunStr)) { - log.info("Running ping on startup (last run: {})", lastRunStr); - sendPing(); - } - } - - /** - * Determines if a task should run immediately on startup. - * Returns false for new installations (no last run recorded) to follow normal schedule. - * Returns true if more than INTERVAL_HOURS have passed since the last run, - * preventing data gaps when the server restarts close to scheduled execution time. - *

- * Example: Telemetry normally runs at 2:00 AM daily. If the server restarts at 1:55 AM, - * the scheduled task would reset and not run until 2:00 AM the next day (48 hours later). - * This method checks if 24+ hours have passed since the last run and executes immediately - * on startup if needed, ensuring data is sent at 1:55 AM instead of waiting another 24 hours. - */ - private boolean shouldRunTask(String lastRunStr) { - if (lastRunStr == null || lastRunStr.isEmpty()) { - return false; - } - try { - Instant lastRun = Instant.parse(lastRunStr); - Instant threshold = Instant.now().minus(INTERVAL_HOURS, ChronoUnit.HOURS); - return lastRun.isBefore(threshold); - } catch (Exception e) { - log.warn("Failed to parse last run timestamp: {}", e.getMessage()); - return false; - } - } - - /** - * Checks if the app version has changed since the last ping. - * Returns true if this is an established installation with a version change. - */ - private boolean hasAppVersionChanged() { - String lastPingVersion = appSettingService.getSettingValue(LAST_PING_APP_VERSION_KEY); - InstallationPing ping = telemetryService.getInstallationPing(); - String currentVersion = ping != null ? ping.getAppVersion() : null; - if (lastPingVersion == null || lastPingVersion.isEmpty() || currentVersion == null) { - return false; - } - return !lastPingVersion.equals(currentVersion); - } -} diff --git a/booklore-api/src/main/java/org/booklore/model/dto/BookloreTelemetry.java b/booklore-api/src/main/java/org/booklore/model/dto/BookloreTelemetry.java deleted file mode 100644 index d14c15e96..000000000 --- a/booklore-api/src/main/java/org/booklore/model/dto/BookloreTelemetry.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.booklore.model.dto; - -import lombok.*; - -import java.util.List; -import java.util.Map; - -@Builder -@Setter -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class BookloreTelemetry { - private int telemetryVersion; - private String installationId; - private String installationDate; - private String appVersion; - - private int totalLibraries; - private long totalBooks; - private long totalAdditionalBookFiles; - private long totalAuthors; - private long totalBookNotes; - private long totalBookmarks; - private int totalShelves; - private int totalMagicShelves; - private int totalCategories; - private int totalTags; - private int totalMoods; - private int totalKoreaderUsers; - - private UserStatistics userStatistics; - private MetadataStatistics metadataStatistics; - private OpdsStatistics opdsStatistics; - private KoboStatistics koboStatistics; - private EmailStatistics emailStatistics; - private BookStatistics bookStatistics; - private List libraryStatisticsList; - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class UserStatistics { - private int totalUsers; - private int totalLocalUsers; - private int totalOidcUsers; - private boolean oidcEnabled; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class MetadataStatistics { - private String[] enabledMetadataProviders; - private String[] enabledReviewMetadataProviders; - private boolean saveMetadataToFile; - private boolean moveFileViaPattern; - private boolean autoBookSearchEnabled; - private boolean similarBookRecommendationsEnabled; - private boolean metadataDownloadOnBookdropEnabled; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class OpdsStatistics { - private boolean opdsEnabled; - private int totalOpdsUsers; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class KoboStatistics { - private int totalKoboUsers; - private int totalHardcoverSyncEnabled; - private int totalAutoAddToShelf; - private boolean convertToKepubEnabled; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class EmailStatistics { - private int totalEmailProviders; - private int totalEmailRecipients; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class BookStatistics { - private long totalBooks; - private Map bookCountByType; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class LibraryStatistics { - private long bookCount; - private int totalLibraryPaths; - private boolean watchEnabled; - private String iconType; - } -} diff --git a/booklore-api/src/main/java/org/booklore/model/dto/InstallationPing.java b/booklore-api/src/main/java/org/booklore/model/dto/InstallationPing.java deleted file mode 100644 index 8cfa25d5d..000000000 --- a/booklore-api/src/main/java/org/booklore/model/dto/InstallationPing.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.booklore.model.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.Instant; - -@Builder -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class InstallationPing { - private int pingVersion; - private String appVersion; - private String installationId; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") - private Instant installationDate; -} diff --git a/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettingKey.java b/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettingKey.java index 9e403172b..1b3b64df8 100644 --- a/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettingKey.java +++ b/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettingKey.java @@ -38,7 +38,6 @@ public enum AppSettingKey { SIMILAR_BOOK_RECOMMENDATION ("similar_book_recommendation", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)), PDF_CACHE_SIZE_IN_MB ("pdf_cache_size_in_mb", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)), MAX_FILE_UPLOAD_SIZE_IN_MB ("max_file_upload_size_in_mb", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)), - TELEMETRY_ENABLED ("telemetryEnabled", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)), // No specific permissions required SIDEBAR_LIBRARY_SORTING ("sidebar_library_sorting", true, false, List.of()), @@ -70,4 +69,4 @@ public enum AppSettingKey { } throw new IllegalArgumentException("Unknown setting key: " + dbKey); } -} \ No newline at end of file +} diff --git a/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettings.java b/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettings.java index 87e5ddb89..ef5699df7 100644 --- a/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettings.java +++ b/booklore-api/src/main/java/org/booklore/model/dto/settings/AppSettings.java @@ -26,7 +26,6 @@ public class AppSettings { private boolean remoteAuthEnabled; private boolean metadataDownloadOnBookdrop; private boolean oidcEnabled; - private boolean telemetryEnabled; private OidcProviderDetails oidcProviderDetails; private OidcAutoProvisionDetails oidcAutoProvisionDetails; private MetadataProviderSettings metadataProviderSettings; @@ -40,4 +39,4 @@ public class AppSettings { private String oidcGroupSyncMode; private boolean oidcForceOnlyMode; private String diskType; -} \ No newline at end of file +} diff --git a/booklore-api/src/main/java/org/booklore/service/TelemetryService.java b/booklore-api/src/main/java/org/booklore/service/TelemetryService.java deleted file mode 100644 index 180697b6a..000000000 --- a/booklore-api/src/main/java/org/booklore/service/TelemetryService.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.booklore.service; - -import org.booklore.model.dto.BookloreTelemetry; -import org.booklore.model.dto.Installation; -import org.booklore.model.dto.InstallationPing; -import org.booklore.model.dto.settings.AppSettings; -import org.booklore.model.dto.settings.MetadataProviderSettings; -import org.booklore.model.dto.settings.MetadataPublicReviewsSettings; -import org.booklore.model.dto.settings.UserSettingKey; -import org.booklore.model.entity.LibraryEntity; -import org.booklore.model.enums.BookFileType; -import org.booklore.model.enums.MetadataProvider; -import org.booklore.model.enums.ProvisioningMethod; -import org.booklore.repository.*; -import org.booklore.service.appsettings.AppSettingService; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class TelemetryService { - - private final VersionService versionService; - private final LibraryRepository libraryRepository; - private final BookRepository bookRepository; - private final BookMarkRepository bookMarkRepository; - private final BookNoteRepository bookNoteRepository; - private final BookAdditionalFileRepository bookAdditionalFileRepository; - private final AuthorRepository authorRepository; - private final ShelfRepository shelfRepository; - private final MagicShelfRepository magicShelfRepository; - private final CategoryRepository categoryRepository; - private final TagRepository tagRepository; - private final MoodRepository moodRepository; - private final UserRepository userRepository; - private final EmailProviderV2Repository emailProviderV2Repository; - private final EmailRecipientV2Repository emailRecipientV2Repository; - private final AppSettingService appSettingService; - private final KoboUserSettingsRepository koboUserSettingsRepository; - private final UserSettingRepository userSettingRepository; - private final KoreaderUserRepository koreaderUserRepository; - private final OpdsUserV2Repository opdsUserV2Repository; - private final InstallationService installationService; - - public InstallationPing getInstallationPing() { - Installation installation = installationService.getOrCreateInstallation(); - - return InstallationPing.builder() - .pingVersion(1) - .appVersion(versionService.appVersion) - .installationId(installation.getId()) - .installationDate(installation.getDate()) - .build(); - } - - public BookloreTelemetry collectTelemetry() { - long totalUsers = userRepository.count(); - long localUsers = userRepository.countByProvisioningMethod(ProvisioningMethod.LOCAL); - long oidcUsers = userRepository.countByProvisioningMethod(ProvisioningMethod.OIDC); - - AppSettings settings = appSettingService.getAppSettings(); - - BookloreTelemetry.BookStatistics bookStatistics = BookloreTelemetry.BookStatistics.builder() - .totalBooks(bookRepository.count()) - .bookCountByType(getBookFileTypeCounts()) - .build(); - - List libraryStatisticsList = libraryRepository.findAll().stream() - .map(this::mapLibraryStatistics) - .collect(Collectors.toList()); - - String[] enabledMetadataProviders = getEnabledMetadataProviders(settings.getMetadataProviderSettings()); - String[] enabledReviewMetadataProviders = getEnabledReviewMetadataProviders(settings.getMetadataPublicReviewsSettings()); - - Installation installation = installationService.getOrCreateInstallation(); - - return BookloreTelemetry.builder() - .telemetryVersion(2) - .installationId(installation.getId()) - .installationDate(installation.getDate() != null ? installation.getDate().toString() : null) - .appVersion(versionService.appVersion) - .totalLibraries((int) libraryRepository.count()) - .totalBooks(bookRepository.count()) - .totalAdditionalBookFiles(bookAdditionalFileRepository.count()) - .totalAuthors(authorRepository.count()) - .totalBookmarks(bookMarkRepository.count()) - .totalBookNotes(bookNoteRepository.count()) - .totalShelves((int) shelfRepository.count()) - .totalMagicShelves((int) magicShelfRepository.count()) - .totalCategories((int) categoryRepository.count()) - .totalTags((int) tagRepository.count()) - .totalMoods((int) moodRepository.count()) - .totalKoreaderUsers((int) koreaderUserRepository.count()) - .userStatistics(BookloreTelemetry.UserStatistics.builder() - .totalUsers((int) totalUsers) - .totalLocalUsers((int) localUsers) - .totalOidcUsers((int) oidcUsers) - .oidcEnabled(oidcUsers > 0) - .build()) - .metadataStatistics(BookloreTelemetry.MetadataStatistics.builder() - .enabledMetadataProviders(enabledMetadataProviders) - .enabledReviewMetadataProviders(enabledReviewMetadataProviders) - .saveMetadataToFile(settings.getMetadataPersistenceSettings().getSaveToOriginalFile().isAnyFormatEnabled()) - .moveFileViaPattern(settings.getMetadataPersistenceSettings().isMoveFilesToLibraryPattern()) - .autoBookSearchEnabled(settings.isAutoBookSearch()) - .similarBookRecommendationsEnabled(settings.isSimilarBookRecommendation()) - .metadataDownloadOnBookdropEnabled(settings.isMetadataDownloadOnBookdrop()) - .build()) - .opdsStatistics(BookloreTelemetry.OpdsStatistics.builder() - .opdsEnabled(settings.isOpdsServerEnabled()) - .totalOpdsUsers((int) opdsUserV2Repository.count()) - .build()) - .emailStatistics(BookloreTelemetry.EmailStatistics.builder() - .totalEmailProviders((int) emailProviderV2Repository.count()) - .totalEmailRecipients((int) emailRecipientV2Repository.count()) - .build()) - .koboStatistics(BookloreTelemetry.KoboStatistics.builder() - .convertToKepubEnabled(settings.getKoboSettings().isConvertToKepub()) - .totalKoboUsers((int) koboUserSettingsRepository.count()) - .totalHardcoverSyncEnabled((int) userSettingRepository.countBySettingKeyAndSettingValue( - UserSettingKey.HARDCOVER_SYNC_ENABLED.getDbKey(), "true")) - .totalAutoAddToShelf((int) koboUserSettingsRepository.countByAutoAddToShelfTrue()) - .build()) - .bookStatistics(bookStatistics) - .libraryStatisticsList(libraryStatisticsList) - .build(); - } - - private Map getBookFileTypeCounts() { - Map countByType = new HashMap<>(); - for (BookFileType type : BookFileType.values()) { - countByType.put(type.name(), bookRepository.countByBookType(type)); - } - return countByType; - } - - private BookloreTelemetry.LibraryStatistics mapLibraryStatistics(LibraryEntity lib) { - return BookloreTelemetry.LibraryStatistics.builder() - .totalLibraryPaths(lib.getLibraryPaths() != null ? lib.getLibraryPaths().size() : 0) - .bookCount(bookRepository.countByLibraryId(lib.getId())) - .watchEnabled(lib.isWatch()) - .iconType(lib.getIconType() != null ? lib.getIconType().name() : null) - .build(); - } - - private String[] getEnabledMetadataProviders(MetadataProviderSettings providers) { - List enabled = new ArrayList<>(); - if (providers.getAmazon() != null && providers.getAmazon().isEnabled()) - enabled.add(MetadataProvider.Amazon.name()); - if (providers.getGoogle() != null && providers.getGoogle().isEnabled()) - enabled.add(MetadataProvider.Google.name()); - if (providers.getGoodReads() != null && providers.getGoodReads().isEnabled()) - enabled.add(MetadataProvider.GoodReads.name()); - if (providers.getHardcover() != null && providers.getHardcover().isEnabled()) - enabled.add(MetadataProvider.Hardcover.name()); - if (providers.getComicvine() != null && providers.getComicvine().isEnabled()) - enabled.add(MetadataProvider.Comicvine.name()); - if (providers.getRanobedb() != null && providers.getRanobedb().isEnabled()) - enabled.add(MetadataProvider.Ranobedb.name()); - if (providers.getDouban() != null && providers.getDouban().isEnabled()) - enabled.add(MetadataProvider.Douban.name()); - if (providers.getLubimyczytac() != null && providers.getLubimyczytac().isEnabled()) - enabled.add(MetadataProvider.Lubimyczytac.name()); - return enabled.toArray(new String[0]); - } - - private String[] getEnabledReviewMetadataProviders(MetadataPublicReviewsSettings reviewSettings) { - List enabled = new ArrayList<>(); - if (reviewSettings.getProviders() != null) { - reviewSettings.getProviders().stream() - .filter(MetadataPublicReviewsSettings.ReviewProviderConfig::isEnabled) - .forEach(cfg -> enabled.add(cfg.getProvider().name())); - } - return enabled.toArray(new String[0]); - } -} diff --git a/booklore-api/src/main/java/org/booklore/service/VersionService.java b/booklore-api/src/main/java/org/booklore/service/VersionService.java index c305df7fe..e7fe224bf 100644 --- a/booklore-api/src/main/java/org/booklore/service/VersionService.java +++ b/booklore-api/src/main/java/org/booklore/service/VersionService.java @@ -21,7 +21,7 @@ public class VersionService { @Value("${app.version:unknown}") String appVersion; - private static final String GITHUB_REPO = "booklore-app/booklore"; + private static final String GITHUB_REPO = "the-booklore/booklore"; private static final String BASE_URI = "https://api.github.com/repos/" + GITHUB_REPO; private static final int MAX_RELEASES = 15; private static final RestClient REST_CLIENT = RestClient.builder() @@ -87,7 +87,7 @@ public class VersionService { if (tag == null || !isVersionGreater(tag, currentVersion)) { continue; } - String url = "https://github.com/booklore-app/booklore" + "/releases/tag/" + tag; + String url = "https://github.com/the-booklore/booklore" + "/releases/tag/" + tag; LocalDateTime published = LocalDateTime.parse(release.path("published_at").asText(), DateTimeFormatter.ISO_DATE_TIME); updates.add(new ReleaseNote(tag, release.path("name").asText(tag), release.path("body").asText(""), url, published)); } diff --git a/booklore-api/src/main/java/org/booklore/service/appsettings/AppSettingService.java b/booklore-api/src/main/java/org/booklore/service/appsettings/AppSettingService.java index f2fd54414..261683e11 100644 --- a/booklore-api/src/main/java/org/booklore/service/appsettings/AppSettingService.java +++ b/booklore-api/src/main/java/org/booklore/service/appsettings/AppSettingService.java @@ -185,7 +185,6 @@ public class AppSettingService { builder.opdsServerEnabled(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.OPDS_SERVER_ENABLED, "false"))); builder.komgaApiEnabled(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.KOMGA_API_ENABLED, "false"))); builder.komgaGroupUnknown(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.KOMGA_GROUP_UNKNOWN, "true"))); - builder.telemetryEnabled(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.TELEMETRY_ENABLED, "true"))); builder.pdfCacheSizeInMb(Integer.parseInt(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.PDF_CACHE_SIZE_IN_MB, "5120"))); builder.maxFileUploadSizeInMb(Integer.parseInt(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.MAX_FILE_UPLOAD_SIZE_IN_MB, "100"))); builder.metadataDownloadOnBookdrop(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.METADATA_DOWNLOAD_ON_BOOKDROP, "true"))); diff --git a/booklore-api/src/main/java/org/booklore/service/kobo/KepubConversionService.java b/booklore-api/src/main/java/org/booklore/service/kobo/KepubConversionService.java index 65d88db12..909dcb53a 100644 --- a/booklore-api/src/main/java/org/booklore/service/kobo/KepubConversionService.java +++ b/booklore-api/src/main/java/org/booklore/service/kobo/KepubConversionService.java @@ -19,7 +19,7 @@ public class KepubConversionService { @Autowired private FileService fileService; - private static final String KEPUBIFY_GITHUB_BASE_URL = "https://github.com/booklore-app/booklore-tools/raw/main/kepubify/"; + private static final String KEPUBIFY_GITHUB_BASE_URL = "https://github.com/the-booklore/booklore-tools/raw/main/kepubify/"; private static final String BIN_DARWIN_ARM64 = "kepubify-darwin-arm64"; private static final String BIN_DARWIN_X64 = "kepubify-darwin-64bit"; diff --git a/booklore-api/src/main/java/org/booklore/service/metadata/parser/ComicvineBookParser.java b/booklore-api/src/main/java/org/booklore/service/metadata/parser/ComicvineBookParser.java index 7b0529076..8ce514889 100644 --- a/booklore-api/src/main/java/org/booklore/service/metadata/parser/ComicvineBookParser.java +++ b/booklore-api/src/main/java/org/booklore/service/metadata/parser/ComicvineBookParser.java @@ -467,7 +467,7 @@ public class ComicvineBookParser implements BookParser, DetailedMetadataProvider log.debug("ComicVine API call #{} to {}", callNumber, endpoint); HttpRequest request = HttpRequest.newBuilder() .uri(uri) - .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)") + .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/the-booklore/booklore)") .GET() .build(); diff --git a/booklore-api/src/main/java/org/booklore/service/metadata/parser/RanobeDbParser.java b/booklore-api/src/main/java/org/booklore/service/metadata/parser/RanobeDbParser.java index ca04afed7..0be273472 100644 --- a/booklore-api/src/main/java/org/booklore/service/metadata/parser/RanobeDbParser.java +++ b/booklore-api/src/main/java/org/booklore/service/metadata/parser/RanobeDbParser.java @@ -132,7 +132,7 @@ public class RanobeDbParser implements BookParser { HttpRequest request = HttpRequest.newBuilder() .uri(uri) - .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)") + .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/the-booklore/booklore)") .GET() .build(); @@ -189,7 +189,7 @@ public class RanobeDbParser implements BookParser { HttpRequest request = HttpRequest.newBuilder() .uri(uri) - .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)") + .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/the-booklore/booklore)") .GET() .build(); diff --git a/booklore-api/src/main/java/org/booklore/service/reader/FfprobeService.java b/booklore-api/src/main/java/org/booklore/service/reader/FfprobeService.java index 600327c3c..762f53606 100644 --- a/booklore-api/src/main/java/org/booklore/service/reader/FfprobeService.java +++ b/booklore-api/src/main/java/org/booklore/service/reader/FfprobeService.java @@ -16,7 +16,7 @@ import java.nio.file.StandardCopyOption; @AllArgsConstructor public class FfprobeService { - private static final String FFPROBE_GITHUB_BASE_URL = "https://github.com/booklore-app/booklore-tools/raw/main/ffprobe/"; + private static final String FFPROBE_GITHUB_BASE_URL = "https://github.com/the-booklore/booklore-tools/raw/main/ffprobe/"; private static final String BIN_DARWIN_ARM64 = "ffprobe-darwin-arm64"; private static final String BIN_DARWIN_X64 = "ffprobe-darwin-64"; diff --git a/booklore-api/src/main/java/org/booklore/util/FileService.java b/booklore-api/src/main/java/org/booklore/util/FileService.java index 8cc283aa9..141fee6af 100644 --- a/booklore-api/src/main/java/org/booklore/util/FileService.java +++ b/booklore-api/src/main/java/org/booklore/util/FileService.java @@ -290,7 +290,7 @@ public class FileService { } HttpHeaders headers = new HttpHeaders(); - headers.set(HttpHeaders.USER_AGENT, "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)"); + headers.set(HttpHeaders.USER_AGENT, "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/the-booklore/booklore)"); headers.set(HttpHeaders.ACCEPT, "image/*"); HttpEntity entity = new HttpEntity<>(headers); diff --git a/booklore-api/src/main/resources/application.yaml b/booklore-api/src/main/resources/application.yaml index 6d909e2e1..ffdd37a20 100644 --- a/booklore-api/src/main/resources/application.yaml +++ b/booklore-api/src/main/resources/application.yaml @@ -19,8 +19,6 @@ app: admin-group: ${REMOTE_AUTH_ADMIN_GROUP} groups-delimiter: ${REMOTE_AUTH_GROUPS_DELIMITER:\\s+} force-disable-oidc: ${FORCE_DISABLE_OIDC:false} - telemetry: - base-url: ${TELEMETRY_BASE_URL:https://telemetry.booklore.org} disk-type: ${DISK_TYPE:LOCAL} server: @@ -99,4 +97,4 @@ logging: org.quartz.simpl.SimpleThreadPool: WARN org.quartz.simpl.RAMJobStore: WARN org.hibernate.orm.connections.pooling: WARN - org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer: ERROR \ No newline at end of file + org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer: ERROR diff --git a/booklore-api/src/test/java/org/booklore/crons/CronServiceTest.java b/booklore-api/src/test/java/org/booklore/crons/CronServiceTest.java deleted file mode 100644 index c76d6aba4..000000000 --- a/booklore-api/src/test/java/org/booklore/crons/CronServiceTest.java +++ /dev/null @@ -1,389 +0,0 @@ -package org.booklore.crons; - -import org.booklore.config.AppProperties; -import org.booklore.model.dto.BookloreTelemetry; -import org.booklore.model.dto.InstallationPing; -import org.booklore.model.dto.settings.AppSettings; -import org.booklore.service.TelemetryService; -import org.booklore.service.appsettings.AppSettingService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.web.client.RestClient; - -import java.time.Instant; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.*; - -class CronServiceTest { - - private AppProperties appProperties; - private TelemetryService telemetryService; - private RestClient restClient; - private AppSettingService appSettingService; - private CronService cronService; - - @BeforeEach - void setUp() { - appProperties = mock(AppProperties.class, RETURNS_DEEP_STUBS); - telemetryService = mock(TelemetryService.class); - restClient = mock(RestClient.class, RETURNS_DEEP_STUBS); - appSettingService = mock(AppSettingService.class); - cronService = new CronService(appProperties, telemetryService, restClient, appSettingService); - - AppSettings defaultSettings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(defaultSettings); - when(defaultSettings.isTelemetryEnabled()).thenReturn(true); - - InstallationPing defaultPing = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(defaultPing); - } - - @Test - void sendTelemetryData_telemetryDisabled_doesNotSend() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(false); - cronService.sendTelemetryData(); - verifyNoInteractions(telemetryService); - verify(appSettingService, never()).saveSetting(anyString(), anyString()); - } - - @Test - void sendTelemetryData_telemetryEnabled_postSuccess_savesSetting() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appProperties.getTelemetry().getBaseUrl()).thenReturn("http://telemetry"); - BookloreTelemetry telemetry = mock(BookloreTelemetry.class); - when(telemetryService.collectTelemetry()).thenReturn(telemetry); - RestClient.RequestBodyUriSpec post = mock(RestClient.RequestBodyUriSpec.class, RETURNS_DEEP_STUBS); - when(restClient.post()).thenReturn(post); - when(post.uri(anyString())).thenReturn(post); - when(post.body(any())).thenReturn(post); - when(post.retrieve().body(String.class)).thenReturn("ok"); - cronService.sendTelemetryData(); - verify(appSettingService).saveSetting(eq("last_telemetry_sent"), anyString()); - } - - @Test - void sendTelemetryData_telemetryEnabled_postFails_doesNotSaveSetting() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appProperties.getTelemetry().getBaseUrl()).thenReturn("http://telemetry"); - BookloreTelemetry telemetry = mock(BookloreTelemetry.class); - when(telemetryService.collectTelemetry()).thenReturn(telemetry); - - CronService spy = spy(cronService); - doReturn(false).when(spy).postData(anyString(), any()); - - spy.sendTelemetryData(); - verify(appSettingService, never()).saveSetting(eq("last_telemetry_sent"), anyString()); - } - - @Test - void sendPing_telemetryDisabled_doesNotSend() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(false); - cronService.sendPing(); - verifyNoInteractions(restClient); - } - - @Test - void sendPing_telemetryEnabled_postSuccess_savesSettings() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appProperties.getTelemetry().getBaseUrl()).thenReturn("http://telemetry"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - RestClient.RequestBodyUriSpec post = mock(RestClient.RequestBodyUriSpec.class, RETURNS_DEEP_STUBS); - when(restClient.post()).thenReturn(post); - when(post.uri(anyString())).thenReturn(post); - when(post.body(any())).thenReturn(post); - when(post.retrieve().body(String.class)).thenReturn("ok"); - cronService.sendPing(); - verify(appSettingService).saveSetting(eq("last_ping_sent"), anyString()); - verify(appSettingService).saveSetting(eq("last_ping_app_version"), eq("1.0.0")); - } - - @Test - void sendPing_telemetryEnabled_postFails_doesNotSaveSettings() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appProperties.getTelemetry().getBaseUrl()).thenReturn("http://telemetry"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - - CronService spy = spy(cronService); - doReturn(false).when(spy).postData(anyString(), any()); - - spy.sendPing(); - verify(appSettingService, never()).saveSetting(eq("last_ping_sent"), anyString()); - verify(appSettingService, never()).saveSetting(eq("last_ping_app_version"), anyString()); - } - - @Test - void shouldRunTask_nullOrEmpty_returnsFalse() { - assertFalse(cronServiceShouldRunTask(null)); - assertFalse(cronServiceShouldRunTask("")); - } - - @Test - void shouldRunTask_invalidTimestamp_returnsFalse() { - assertFalse(cronServiceShouldRunTask("not-a-timestamp")); - } - - @Test - void shouldRunTask_recentTimestamp_returnsFalse() { - String now = Instant.now().toString(); - assertFalse(cronServiceShouldRunTask(now)); - } - - @Test - void shouldRunTask_oldTimestamp_returnsTrue() { - String old = Instant.now().minusSeconds(60 * 60 * 25).toString(); - assertTrue(cronServiceShouldRunTask(old)); - } - - boolean cronServiceShouldRunTask(String lastRunStr) { - return invokeShouldRunTask(cronService, lastRunStr); - } - - boolean invokeShouldRunTask(CronService cronService, String lastRunStr) { - try { - var m = CronService.class.getDeclaredMethod("shouldRunTask", String.class); - m.setAccessible(true); - return (boolean) m.invoke(cronService, lastRunStr); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - void hasAppVersionChanged_noLastPing_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn(null); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_lastPingEmpty_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn(""); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_sameVersion_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_differentVersion_returnsTrue() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("2.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - assertTrue(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_nullPing_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - when(telemetryService.getInstallationPing()).thenReturn(null); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - boolean invokeHasAppVersionChanged(CronService cronService) { - try { - var m = CronService.class.getDeclaredMethod("hasAppVersionChanged"); - m.setAccessible(true); - return (boolean) m.invoke(cronService); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - void checkAndRunTelemetry_telemetryDisabled_doesNothing() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(false); - cronService.initScheduledTasks(); - verify(appSettingService, never()).getSettingValue("last_telemetry_sent"); - } - - @Test - void checkAndRunTelemetry_shouldRunTaskTrue_callsSendTelemetryData() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appSettingService.getSettingValue("last_telemetry_sent")).thenReturn(Instant.now().minusSeconds(60 * 60 * 25).toString()); - CronService spy = spy(cronService); - doNothing().when(spy).sendTelemetryData(); - // Ensure getInstallationPing returns non-null for ping checks - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - spy.initScheduledTasks(); - verify(spy).sendTelemetryData(); - } - - @Test - void checkAndRunPing_appVersionChanged_callsSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("2.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy).sendPing(); - } - - @Test - void checkAndRunPing_shouldRunTaskTrue_callsSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - when(appSettingService.getSettingValue("last_ping_sent")).thenReturn(Instant.now().minusSeconds(60 * 60 * 25).toString()); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy).sendPing(); - } - - @Test - void checkAndRunPing_shouldRunTaskFalse_doesNotCallSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - when(appSettingService.getSettingValue("last_ping_sent")).thenReturn(Instant.now().toString()); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy, never()).sendPing(); - } - - @Test - void sendTelemetryData_nullSettings_doesNotThrowOrSend() { - when(appSettingService.getAppSettings()).thenReturn(null); - cronService.sendTelemetryData(); - verifyNoInteractions(telemetryService); - verify(appSettingService, never()).saveSetting(anyString(), anyString()); - } - - @Test - void sendTelemetryData_telemetryEnabled_collectTelemetryReturnsNull_doesNotThrow() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appProperties.getTelemetry().getBaseUrl()).thenReturn("http://telemetry"); - when(telemetryService.collectTelemetry()).thenReturn(null); - - CronService spy = spy(cronService); - doReturn(false).when(spy).postData(anyString(), any()); - - spy.sendTelemetryData(); - verify(appSettingService, never()).saveSetting(eq("last_telemetry_sent"), anyString()); - } - - @Test - void sendPing_nullPing_doesNotSave() { - when(telemetryService.getInstallationPing()).thenReturn(null); - cronService.sendPing(); - verify(appSettingService, never()).saveSetting(anyString(), anyString()); - } - - @Test - void shouldRunTask_farFutureTimestamp_returnsFalse() { - String future = Instant.now().plusSeconds(60 * 60 * 25).toString(); - assertFalse(cronServiceShouldRunTask(future)); - } - - @Test - void shouldRunTask_epoch_returnsTrue() { - String epoch = Instant.EPOCH.toString(); - assertTrue(cronServiceShouldRunTask(epoch)); - } - - @Test - void hasAppVersionChanged_lastPingNullCurrentNull_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn(null); - when(telemetryService.getInstallationPing()).thenReturn(null); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_lastPingEmptyCurrentNull_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn(""); - when(telemetryService.getInstallationPing()).thenReturn(null); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void hasAppVersionChanged_lastPingNonEmptyCurrentNull_returnsFalse() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - when(telemetryService.getInstallationPing()).thenReturn(null); - assertFalse(invokeHasAppVersionChanged(cronService)); - } - - @Test - void checkAndRunTelemetry_nullSettings_doesNothing() { - when(appSettingService.getAppSettings()).thenReturn(null); - cronService.initScheduledTasks(); - verify(appSettingService, never()).getSettingValue("last_telemetry_sent"); - } - - @Test - void checkAndRunTelemetry_shouldRunTaskFalse_doesNotCallSendTelemetryData() { - AppSettings settings = mock(AppSettings.class); - when(appSettingService.getAppSettings()).thenReturn(settings); - when(settings.isTelemetryEnabled()).thenReturn(true); - when(appSettingService.getSettingValue("last_telemetry_sent")).thenReturn(Instant.now().toString()); - CronService spy = spy(cronService); - doNothing().when(spy).sendTelemetryData(); - spy.initScheduledTasks(); - verify(spy, never()).sendTelemetryData(); - } - - @Test - void checkAndRunPing_nullLastPingVersion_doesNotCallSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn(null); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy, never()).sendPing(); - } - - @Test - void checkAndRunPing_nullPing_doesNotCallSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - when(telemetryService.getInstallationPing()).thenReturn(null); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy, never()).sendPing(); - } - - @Test - void checkAndRunPing_shouldRunTaskFalseAndNoVersionChange_doesNotCallSendPing() { - when(appSettingService.getSettingValue("last_ping_app_version")).thenReturn("1.0.0"); - InstallationPing ping = InstallationPing.builder().appVersion("1.0.0").build(); - when(telemetryService.getInstallationPing()).thenReturn(ping); - when(appSettingService.getSettingValue("last_ping_sent")).thenReturn(Instant.now().toString()); - CronService spy = spy(cronService); - doNothing().when(spy).sendPing(); - spy.initScheduledTasks(); - verify(spy, never()).sendPing(); - } -} diff --git a/booklore-ui/package-lock.json b/booklore-ui/package-lock.json index 9500f50a2..6c9c8d1e1 100644 --- a/booklore-ui/package-lock.json +++ b/booklore-ui/package-lock.json @@ -47,7 +47,7 @@ "devDependencies": { "@analogjs/vite-plugin-angular": "^2.3.0", "@analogjs/vitest-angular": "^2.3.0", - "@angular/build": "^21.2.0", + "@angular/build": "^21.2.7", "@angular/cli": "^21.2.0", "@angular/compiler-cli": "^21.2.4", "@types/dompurify": "^3.2.0", @@ -343,13 +343,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.0.tgz", - "integrity": "sha512-kYFwTNzToG2SJMxj2f41w3QRtdqlrFuF+bpZrtIaHOP078Ktld8EPIp9KqB0Y46Vvs69ifby5Q1/wPD9wA3iaw==", + "version": "0.2102.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.7.tgz", + "integrity": "sha512-4K/5hln9iaPEt3F/NyYqncNLvYpzSjRslEkHl2xIgZwQsIFHEvhnDRBYj2/oatURQhBqO/Yu15z/icVOYLxuTg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0", + "@angular-devkit/core": "21.2.7", "rxjs": "7.8.2" }, "bin": { @@ -362,16 +362,16 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.0.tgz", - "integrity": "sha512-HZdTn46Ca6qbb9Zef8R/+TWsk6mNKRm4rJyL3PxHP6HnVCwSPNZ0LNN9BjVREBs+UlRdXqBGFBZh5D1nBgu5GQ==", + "version": "21.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.7.tgz", + "integrity": "sha512-DONYY5u4IENO2qpd23mODaE4JI2EIohWV1kuJnsU9HIcm5wN714QB2z9WY/s4gLfUiAMIUu/8lpnW/0kOQZAnQ==", "dev": true, "license": "MIT", "dependencies": { "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, @@ -390,13 +390,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.0.tgz", - "integrity": "sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==", + "version": "21.2.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.7.tgz", + "integrity": "sha512-LYAjjUI1qM7pR/sd0yYt8OLA6ljOOXjcfzV40I5XQNmhAxq90YYS5xwMcixOmWX+z5zvCYGvPXvJGWjzio6SUg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0", + "@angular-devkit/core": "21.2.7", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.3.0", @@ -517,14 +517,14 @@ } }, "node_modules/@angular/build": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.0.tgz", - "integrity": "sha512-K0EqiHz2y7TSyD4adWD0+C/P9khKlrsSWavXWxGRvoSJC/H3I3SK5Z6BWwftBibXR1Fis7njwvl5IGAlQrDchA==", + "version": "21.2.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.7.tgz", + "integrity": "sha512-FpSkFqpsJtdN1cROekVYkmeV1QepdP+/d7fyYQEuNmlOlyqXSDh9qJmy4iL9VNbAU0rk+vFCtYM86rO7Pt9cSw==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.0", + "@angular-devkit/architect": "0.2102.7", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -540,15 +540,15 @@ "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "piscina": "5.1.4", "rolldown": "1.0.0-rc.4", "sass": "1.97.3", "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", - "undici": "7.22.0", - "vite": "7.3.1", + "undici": "7.24.4", + "vite": "7.3.2", "watchpack": "2.5.1" }, "engines": { @@ -567,7 +567,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.0", + "@angular/ssr": "^21.2.7", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -646,19 +646,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.0.tgz", - "integrity": "sha512-yaGEpckqgOemcHkoWeH92i9eNrcbr9iE/dnxL+Du6s9spTAXJ2jjtYfszhmowuQZkCK5rjecMb8ctNtHlaGCjg==", + "version": "21.2.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.7.tgz", + "integrity": "sha512-N/wj8fFRB718efIFYpwnYfy+MecZREZXsUNMTVndFLH6T0jCheb9PVetR6jsyZp6h46USNPOmJYJ/9255lME+Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.0", - "@angular-devkit/core": "21.2.0", - "@angular-devkit/schematics": "21.2.0", + "@angular-devkit/architect": "0.2102.7", + "@angular-devkit/core": "21.2.7", + "@angular-devkit/schematics": "21.2.7", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.0", + "@schematics/angular": "21.2.7", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.48.1", "ini": "6.0.0", @@ -2057,9 +2057,9 @@ "optional": true }, "node_modules/@hono/node-server": { - "version": "1.19.10", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz", - "integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "dev": true, "license": "MIT", "engines": { @@ -4380,14 +4380,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.0.tgz", - "integrity": "sha512-GQUIeGzZwCT9/W5MAkKnkwETROPbA1eRmy3JF56jLmvr95tJnypGOG8jGYy0d+tcEVujIouh48r4J3bJQg5mrw==", + "version": "21.2.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.7.tgz", + "integrity": "sha512-aqEj3RyBtmH+41HZvrbfrpCo0e+0NzwyQyNSC/wLDShVqoidBtPbEdHU1FZ4+ni41da7rI3F12gUuAHws27kMA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.0", - "@angular-devkit/schematics": "21.2.0", + "@angular-devkit/core": "21.2.7", + "@angular-devkit/schematics": "21.2.7", "jsonc-parser": "3.3.1" }, "engines": { @@ -5428,9 +5428,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6944,9 +6944,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7188,9 +7188,9 @@ } }, "node_modules/hono": { - "version": "4.12.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", - "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "dev": true, "license": "MIT", "engines": { @@ -7886,9 +7886,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -8107,9 +8107,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -9070,9 +9070,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -9103,9 +9103,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -10482,9 +10482,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/booklore-ui/package.json b/booklore-ui/package.json index 19c7e24f4..33478494e 100644 --- a/booklore-ui/package.json +++ b/booklore-ui/package.json @@ -51,7 +51,7 @@ "devDependencies": { "@analogjs/vite-plugin-angular": "^2.3.0", "@analogjs/vitest-angular": "^2.3.0", - "@angular/build": "^21.2.0", + "@angular/build": "^21.2.7", "@angular/cli": "^21.2.0", "@angular/compiler-cli": "^21.2.4", "@types/dompurify": "^3.2.0", diff --git a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.html b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.html index 75174a584..10195de62 100644 --- a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.html +++ b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.html @@ -220,33 +220,6 @@

- -
-
-

- - {{ t('telemetry.sectionTitle') }} - -

-
-
-
-
-
- - - -
-

- - {{ t('telemetry.telemetryDesc') }} -

-
-
-
-
diff --git a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts index 0276f5c88..3f315a351 100644 --- a/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts +++ b/booklore-ui/src/app/features/settings/global-preferences/global-preferences.component.ts @@ -12,7 +12,6 @@ import {AppSettingKey, AppSettings, CoverCroppingSettings} from '../../../shared import {filter, take} from 'rxjs/operators'; import {InputText} from 'primeng/inputtext'; import {Slider} from 'primeng/slider'; -import {ExternalDocLinkComponent} from '../../../shared/components/external-doc-link/external-doc-link.component'; import {TranslocoDirective, TranslocoPipe, TranslocoService} from '@jsverse/transloco'; export const SUPPORT_ANIMATION_KEY = 'booklore-support-animation'; @@ -27,7 +26,6 @@ export const SUPPORT_ANIMATION_KEY = 'booklore-support-animation'; InputText, Slider, SplitButton, - ExternalDocLinkComponent, TranslocoDirective, TranslocoPipe ], @@ -38,8 +36,7 @@ export class GlobalPreferencesComponent implements OnInit { toggles = { autoBookSearch: false, - similarBookRecommendation: false, - enableTelemetry: true, + similarBookRecommendation: false }; supportButtonAnimation = localStorage.getItem(SUPPORT_ANIMATION_KEY) !== 'false'; @@ -81,7 +78,6 @@ export class GlobalPreferencesComponent implements OnInit { } this.toggles.autoBookSearch = settings.autoBookSearch ?? false; this.toggles.similarBookRecommendation = settings.similarBookRecommendation ?? false; - this.toggles.enableTelemetry = settings?.telemetryEnabled ?? true; }); } @@ -89,8 +85,7 @@ export class GlobalPreferencesComponent implements OnInit { this.toggles[settingKey] = checked; const toggleKeyMap: Record = { autoBookSearch: AppSettingKey.AUTO_BOOK_SEARCH, - similarBookRecommendation: AppSettingKey.SIMILAR_BOOK_RECOMMENDATION, - enableTelemetry: AppSettingKey.TELEMETRY_ENABLED, + similarBookRecommendation: AppSettingKey.SIMILAR_BOOK_RECOMMENDATION }; const keyToSend = toggleKeyMap[settingKey]; if (keyToSend) { diff --git a/booklore-ui/src/app/shared/components/external-doc-link/external-doc-link.component.ts b/booklore-ui/src/app/shared/components/external-doc-link/external-doc-link.component.ts index f6f74ce30..102f659be 100644 --- a/booklore-ui/src/app/shared/components/external-doc-link/external-doc-link.component.ts +++ b/booklore-ui/src/app/shared/components/external-doc-link/external-doc-link.component.ts @@ -3,7 +3,7 @@ import {Tooltip} from 'primeng/tooltip'; export type DocType = 'kobo' | 'opds' | 'metadataManager' | 'koReader' | 'email' | 'amazonCookie' | 'fetchConfig' | 'hardcover' | 'taskManagement' | 'fileNamePatterns' - | 'authentication' | 'telemetry'; + | 'authentication'; @Component({ selector: 'app-external-doc-link', @@ -38,8 +38,7 @@ export class ExternalDocLinkComponent { fetchConfig: `${this.BASE_URL}/metadata/metadata-fetch-configuration`, taskManagement: `${this.BASE_URL}/tools/task-manager`, fileNamePatterns: `${this.BASE_URL}/metadata/file-naming-patterns`, - authentication: `${this.BASE_URL}/authentication/overview#setting-up-oidc`, - telemetry: `${this.BASE_URL}/tools/telemetry` + authentication: `${this.BASE_URL}/authentication/overview#setting-up-oidc` }; @Input() docType!: DocType; diff --git a/booklore-ui/src/app/shared/components/github-support-dialog/github-support-dialog.html b/booklore-ui/src/app/shared/components/github-support-dialog/github-support-dialog.html index d9a55631f..de48f7ce9 100644 --- a/booklore-ui/src/app/shared/components/github-support-dialog/github-support-dialog.html +++ b/booklore-ui/src/app/shared/components/github-support-dialog/github-support-dialog.html @@ -24,7 +24,7 @@

- +
{{ t('starTitle') }} diff --git a/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts b/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts index c3119c31b..46c4458da 100644 --- a/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts +++ b/booklore-ui/src/app/shared/layout/component/layout-menu/app.menu.component.ts @@ -252,8 +252,8 @@ export class AppMenuComponent implements OnInit { getVersionUrl(version: string | undefined): string { if (!version) return '#'; return version.startsWith('v') - ? `https://github.com/booklore-app/booklore/releases/tag/${version}` - : `https://github.com/booklore-app/booklore/commit/${version}`; + ? `https://github.com/the-booklore/booklore/releases/tag/${version}` + : `https://github.com/the-booklore/booklore/commit/${version}`; } isSemanticVersion(version: string | undefined): boolean { diff --git a/booklore-ui/src/app/shared/model/app-settings.model.ts b/booklore-ui/src/app/shared/model/app-settings.model.ts index a7c1b5835..968257496 100644 --- a/booklore-ui/src/app/shared/model/app-settings.model.ts +++ b/booklore-ui/src/app/shared/model/app-settings.model.ts @@ -192,7 +192,6 @@ export interface AppSettings { koboSettings: KoboSettings; coverCroppingSettings: CoverCroppingSettings; metadataDownloadOnBookdrop: boolean; - telemetryEnabled: boolean; metadataProviderSpecificFields: MetadataProviderSpecificFields; oidcSessionDurationHours: number | null; oidcGroupSyncMode: string | null; @@ -242,7 +241,6 @@ export enum AppSettingKey { METADATA_PUBLIC_REVIEWS_SETTINGS = 'METADATA_PUBLIC_REVIEWS_SETTINGS', KOBO_SETTINGS = 'KOBO_SETTINGS', COVER_CROPPING_SETTINGS = 'COVER_CROPPING_SETTINGS', - TELEMETRY_ENABLED = 'TELEMETRY_ENABLED', METADATA_PROVIDER_SPECIFIC_FIELDS = 'METADATA_PROVIDER_SPECIFIC_FIELDS', OIDC_SESSION_DURATION_HOURS = 'OIDC_SESSION_DURATION_HOURS', OIDC_GROUP_SYNC_MODE = 'OIDC_GROUP_SYNC_MODE', diff --git a/booklore-ui/src/i18n/da/settings-application.json b/booklore-ui/src/i18n/da/settings-application.json index ce5a469a0..a3538c9e5 100644 --- a/booklore-ui/src/i18n/da/settings-application.json +++ b/booklore-ui/src/i18n/da/settings-application.json @@ -1,51 +1,46 @@ { - "title": "", - "description": "", - "covers": { - "sectionTitle": "", - "regenerate": "", - "regenerateAllBtn": "", - "regenerateMissingBtn": "", - "regenerateDesc": "", - "regenerateStarted": "", - "regenerateStartedDetail": "", - "regenerateError": "", - "verticalCropping": "", - "verticalCroppingDesc": "", - "horizontalCropping": "", - "horizontalCroppingDesc": "", - "aspectRatio": "", - "aspectRatioDesc": "", - "smartCropping": "", - "smartCroppingDesc": "" - }, - "search": { - "sectionTitle": "", - "autoBookSearch": "", - "autoBookSearchDesc": "", - "similarBook": "", - "similarBookDesc": "" - }, - "fileManagement": { - "sectionTitle": "", - "maxUploadSize": "", - "maxUploadPlaceholder": "", - "maxUploadDesc": "", - "restartWarning": "", - "invalidInput": "", - "invalidInputDetail": "" - }, - "appearance": { - "sectionTitle": "", - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "" - }, - "telemetry": { - "sectionTitle": "", - "enableTelemetry": "", - "telemetryDesc": "" - }, - "settingsSaved": "", - "settingsSavedDetail": "", - "settingsError": "" + "title": "", + "description": "", + "covers": { + "sectionTitle": "", + "regenerate": "", + "regenerateAllBtn": "", + "regenerateMissingBtn": "", + "regenerateDesc": "", + "regenerateStarted": "", + "regenerateStartedDetail": "", + "regenerateError": "", + "verticalCropping": "", + "verticalCroppingDesc": "", + "horizontalCropping": "", + "horizontalCroppingDesc": "", + "aspectRatio": "", + "aspectRatioDesc": "", + "smartCropping": "", + "smartCroppingDesc": "" + }, + "search": { + "sectionTitle": "", + "autoBookSearch": "", + "autoBookSearchDesc": "", + "similarBook": "", + "similarBookDesc": "" + }, + "fileManagement": { + "sectionTitle": "", + "maxUploadSize": "", + "maxUploadPlaceholder": "", + "maxUploadDesc": "", + "restartWarning": "", + "invalidInput": "", + "invalidInputDetail": "" + }, + "appearance": { + "sectionTitle": "", + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "" + }, + "settingsSaved": "", + "settingsSavedDetail": "", + "settingsError": "" } diff --git a/booklore-ui/src/i18n/de/settings-application.json b/booklore-ui/src/i18n/de/settings-application.json index c2ba7e3fd..064cc1864 100644 --- a/booklore-ui/src/i18n/de/settings-application.json +++ b/booklore-ui/src/i18n/de/settings-application.json @@ -1,52 +1,47 @@ { - "title": "Globale Einstellungen", - "description": "Konfigurieren Sie globale Einstellungen fรผr Ihre Booklore-Instanz, einschlieรŸlich Coverbild-Verarbeitung, Sucheinstellungen und Dateiupload-Limits.", - "covers": { - "sectionTitle": "Buchcover-Bild", - "regenerate": "Cover neu generieren", - "regenerateBtn": "Neu generieren", - "regenerateDesc": "Generiert Coverbilder fรผr alle Bรผcher aus den in der Datei eingebetteten Covern neu. Verwenden Sie โ€žFehlende neu generierenโ€œ, um nur Cover fรผr Bรผcher zu erstellen, die noch kein Cover haben.", - "regenerateStarted": "Cover-Neugenerierung gestartet", - "regenerateStartedDetail": "Buchcover werden neu generiert.", - "regenerateError": "Cover-Neugenerierung konnte nicht gestartet werden.", - "verticalCropping": "Vertikaler Cover-Zuschnitt", - "verticalCroppingDesc": "Extrem hohe Bilder (wie Web-Comics) automatisch von oben zuschneiden, um brauchbare Cover-Miniaturansichten zu erstellen.", - "horizontalCropping": "Horizontaler Cover-Zuschnitt", - "horizontalCroppingDesc": "Extrem breite Bilder automatisch von links zuschneiden, um brauchbare Cover-Miniaturansichten zu erstellen.", - "aspectRatio": "Seitenverhรคltnis-Schwellenwert: {{value}}", - "aspectRatioDesc": "Bilder mit Seitenverhรคltnissen รผber diesem Schwellenwert werden zugeschnitten. Ein Wert von 2,5 bedeutet, dass Bilder, die mehr als 2,5-mal hรถher (oder breiter) als normal sind, zugeschnitten werden.", - "smartCropping": "Intelligenter Zuschnitt", - "smartCroppingDesc": "GleichmรครŸige Farbbereiche beim Zuschnitt รผberspringen. Fokussiert das Coverbild auf den relevantesten Inhalt.", - "regenerateAllBtn": "Alle neu generieren", - "regenerateMissingBtn": "Fehlende neu generieren" - }, - "search": { - "sectionTitle": "Suche & Empfehlungen", - "autoBookSearch": "Automatische Buchsuche", - "autoBookSearchDesc": "Versucht automatisch Metadaten abzugleichen, wenn das Buchinformationspanel geรถffnet wird.", - "similarBook": "Buchempfehlungen", - "similarBookDesc": "Aktiviert oder deaktiviert Buchempfehlungen basierend auf Ihrer Bibliothek." - }, - "fileManagement": { - "sectionTitle": "Dateiverwaltung", - "maxUploadSize": "Maximale Dateiupload-GrรถรŸe", - "maxUploadPlaceholder": "Max. GrรถรŸe", - "maxUploadDesc": "Legt die maximal erlaubte GrรถรŸe (in MB) pro hochgeladener Datei fest. Gilt fรผr alle unterstรผtzten Formate.", - "restartWarning": "ร„nderungen werden nach einem Neustart des Servers wirksam", - "invalidInput": "Ungรผltige Eingabe", - "invalidInputDetail": "Bitte geben Sie eine gรผltige maximale Dateiupload-GrรถรŸe in MB ein." - }, - "telemetry": { - "sectionTitle": "Telemetrie", - "enableTelemetry": "Telemetrie aktivieren", - "telemetryDesc": "Helfen Sie Booklore zu verbessern, indem Sie anonyme Nutzungsstatistiken teilen. Diese Daten helfen den Entwicklern zu sehen, welche Funktionen am meisten genutzt werden, Fehler zu erkennen und Leistungsprobleme zu identifizieren, damit sie wissen, woran sie als nรคchstes arbeiten sollten. Es werden niemals persรถnliche Informationen, Buchinhalte oder andere identifizierbare Daten gesendet. Daten werden automatisch alle 24 Stunden an den Booklore-Server gesendet. Es ist absolut sicher und sehr hilfreich fรผr die Entwickler." - }, - "settingsSaved": "Einstellungen gespeichert", - "settingsSavedDetail": "Die Einstellungen wurden erfolgreich gespeichert!", - "settingsError": "Beim Speichern der Einstellungen ist ein Fehler aufgetreten.", - "appearance": { - "supportButtonAnimation": "Animation der Support-Schaltflรคche", - "supportButtonAnimationDesc": "Zeigt den animierten Herz-Effekt auf der Support-Schaltflรคche in der oberen Leiste an. Wenn Sie diese Option deaktivieren, bleibt die Schaltflรคche sichtbar, jedoch wird die Animation deaktiviert.", - "sectionTitle": "Erscheinungsbild" - } + "title": "Globale Einstellungen", + "description": "Konfigurieren Sie globale Einstellungen fรผr Ihre Booklore-Instanz, einschlieรŸlich Coverbild-Verarbeitung, Sucheinstellungen und Dateiupload-Limits.", + "covers": { + "sectionTitle": "Buchcover-Bild", + "regenerate": "Cover neu generieren", + "regenerateBtn": "Neu generieren", + "regenerateDesc": "Generiert Coverbilder fรผr alle Bรผcher aus den in der Datei eingebetteten Covern neu. Verwenden Sie โ€žFehlende neu generierenโ€œ, um nur Cover fรผr Bรผcher zu erstellen, die noch kein Cover haben.", + "regenerateStarted": "Cover-Neugenerierung gestartet", + "regenerateStartedDetail": "Buchcover werden neu generiert.", + "regenerateError": "Cover-Neugenerierung konnte nicht gestartet werden.", + "verticalCropping": "Vertikaler Cover-Zuschnitt", + "verticalCroppingDesc": "Extrem hohe Bilder (wie Web-Comics) automatisch von oben zuschneiden, um brauchbare Cover-Miniaturansichten zu erstellen.", + "horizontalCropping": "Horizontaler Cover-Zuschnitt", + "horizontalCroppingDesc": "Extrem breite Bilder automatisch von links zuschneiden, um brauchbare Cover-Miniaturansichten zu erstellen.", + "aspectRatio": "Seitenverhรคltnis-Schwellenwert: {{value}}", + "aspectRatioDesc": "Bilder mit Seitenverhรคltnissen รผber diesem Schwellenwert werden zugeschnitten. Ein Wert von 2,5 bedeutet, dass Bilder, die mehr als 2,5-mal hรถher (oder breiter) als normal sind, zugeschnitten werden.", + "smartCropping": "Intelligenter Zuschnitt", + "smartCroppingDesc": "GleichmรครŸige Farbbereiche beim Zuschnitt รผberspringen. Fokussiert das Coverbild auf den relevantesten Inhalt.", + "regenerateAllBtn": "Alle neu generieren", + "regenerateMissingBtn": "Fehlende neu generieren" + }, + "search": { + "sectionTitle": "Suche & Empfehlungen", + "autoBookSearch": "Automatische Buchsuche", + "autoBookSearchDesc": "Versucht automatisch Metadaten abzugleichen, wenn das Buchinformationspanel geรถffnet wird.", + "similarBook": "Buchempfehlungen", + "similarBookDesc": "Aktiviert oder deaktiviert Buchempfehlungen basierend auf Ihrer Bibliothek." + }, + "fileManagement": { + "sectionTitle": "Dateiverwaltung", + "maxUploadSize": "Maximale Dateiupload-GrรถรŸe", + "maxUploadPlaceholder": "Max. GrรถรŸe", + "maxUploadDesc": "Legt die maximal erlaubte GrรถรŸe (in MB) pro hochgeladener Datei fest. Gilt fรผr alle unterstรผtzten Formate.", + "restartWarning": "ร„nderungen werden nach einem Neustart des Servers wirksam", + "invalidInput": "Ungรผltige Eingabe", + "invalidInputDetail": "Bitte geben Sie eine gรผltige maximale Dateiupload-GrรถรŸe in MB ein." + }, + "settingsSaved": "Einstellungen gespeichert", + "settingsSavedDetail": "Die Einstellungen wurden erfolgreich gespeichert!", + "settingsError": "Beim Speichern der Einstellungen ist ein Fehler aufgetreten.", + "appearance": { + "supportButtonAnimation": "Animation der Support-Schaltflรคche", + "supportButtonAnimationDesc": "Zeigt den animierten Herz-Effekt auf der Support-Schaltflรคche in der oberen Leiste an. Wenn Sie diese Option deaktivieren, bleibt die Schaltflรคche sichtbar, jedoch wird die Animation deaktiviert.", + "sectionTitle": "Erscheinungsbild" + } } diff --git a/booklore-ui/src/i18n/en/settings-application.json b/booklore-ui/src/i18n/en/settings-application.json index 1f4b57537..201304d09 100644 --- a/booklore-ui/src/i18n/en/settings-application.json +++ b/booklore-ui/src/i18n/en/settings-application.json @@ -40,11 +40,6 @@ "supportButtonAnimation": "Support Button Animation", "supportButtonAnimationDesc": "Show the animated heart effect on the support button in the top bar. Disabling this keeps the button visible but removes the animation." }, - "telemetry": { - "sectionTitle": "Telemetry", - "enableTelemetry": "Enable Telemetry", - "telemetryDesc": "Help improve Booklore by sharing anonymous usage statistics. This data lets the developers see which features are most used, identify bugs, and spot performance issues so they know what to work on next. No personal information, book content, or any identifiable data is ever sent. Data is sent to the Booklore server automatically once every 24 hours. It's completely safe and very helpful to the developers." - }, "settingsSaved": "Settings Saved", "settingsSavedDetail": "The settings were successfully saved!", "settingsError": "There was an error saving the settings." diff --git a/booklore-ui/src/i18n/es/settings-application.json b/booklore-ui/src/i18n/es/settings-application.json index fad7183d1..ea1d31917 100644 --- a/booklore-ui/src/i18n/es/settings-application.json +++ b/booklore-ui/src/i18n/es/settings-application.json @@ -1,51 +1,46 @@ { - "title": "Preferencias globales", - "description": "Configura los ajustes globales de tu instancia de Booklore, incluyendo el manejo de imรกgenes de portada, preferencias de bรบsqueda y lรญmites de carga de archivos.", - "covers": { - "sectionTitle": "Imagen de portada del libro", - "regenerate": "Regenerar portadas", - "regenerateAllBtn": "Regenerar todas", - "regenerateMissingBtn": "Regenerar faltantes", - "regenerateDesc": "Regenera las imรกgenes de portada de todos los libros a partir de las portadas incrustadas en el archivo. Usa \"Regenerar faltantes\" para generar portadas solo para libros que aรบn no tienen una.", - "regenerateStarted": "Regeneraciรณn de portadas iniciada", - "regenerateStartedDetail": "Las portadas de los libros se estรกn regenerando.", - "regenerateError": "Error al iniciar la regeneraciรณn de portadas.", - "verticalCropping": "Recorte vertical de portada", - "verticalCroppingDesc": "Recortar automรกticamente imรกgenes extremadamente altas (como webcรณmics) desde la parte superior para crear miniaturas de portada utilizables.", - "horizontalCropping": "Recorte horizontal de portada", - "horizontalCroppingDesc": "Recortar automรกticamente imรกgenes extremadamente anchas desde la izquierda para crear miniaturas de portada utilizables.", - "aspectRatio": "Umbral de relaciรณn de aspecto: {{value}}", - "aspectRatioDesc": "Las imรกgenes con relaciones de aspecto que excedan este umbral serรกn recortadas. Un valor de 2.5 significa que las imรกgenes mรกs de 2.5 veces mรกs altas (o anchas) de lo normal serรกn recortadas.", - "smartCropping": "Recorte inteligente", - "smartCroppingDesc": "Omitir regiones de color uniforme al determinar dรณnde recortar. Enfoca la imagen de portada en el contenido mรกs relevante." - }, - "search": { - "sectionTitle": "Bรบsqueda y recomendaciones", - "autoBookSearch": "Bรบsqueda automรกtica de libros", - "autoBookSearchDesc": "Intenta automรกticamente emparejar metadatos cuando se abre el panel de informaciรณn del libro.", - "similarBook": "Recomendaciรณn de libros similares", - "similarBookDesc": "Habilita o deshabilita las recomendaciones de libros similares basadas en tu biblioteca." - }, - "fileManagement": { - "sectionTitle": "Gestiรณn de archivos", - "maxUploadSize": "Tamaรฑo mรกximo de carga de archivo", - "maxUploadPlaceholder": "Tamaรฑo mรกximo", - "maxUploadDesc": "Define el tamaรฑo mรกximo permitido (en MB) para cada archivo cargado. Se aplica a todos los formatos de archivo compatibles.", - "restartWarning": "Los cambios surtirรกn efecto despuรฉs de reiniciar el servidor", - "invalidInput": "Entrada invรกlida", - "invalidInputDetail": "Introduce un tamaรฑo mรกximo de carga de archivo vรกlido en MB." - }, - "appearance": { - "sectionTitle": "Apariencia", - "supportButtonAnimation": "Animaciรณn del botรณn de apoyo", - "supportButtonAnimationDesc": "Muestra el efecto animado de corazรณn en el botรณn de apoyo en la barra superior. Desactivar esto mantiene el botรณn visible pero elimina la animaciรณn." - }, - "telemetry": { - "sectionTitle": "Telemetrรญa", - "enableTelemetry": "Habilitar telemetrรญa", - "telemetryDesc": "Ayuda a mejorar Booklore compartiendo estadรญsticas de uso anรณnimas. Estos datos permiten a los desarrolladores ver quรฉ funciones se usan mรกs, identificar errores y detectar problemas de rendimiento para saber en quรฉ trabajar a continuaciรณn. Nunca se envรญa informaciรณn personal, contenido de libros ni datos identificables. Los datos se envรญan al servidor de Booklore automรกticamente una vez cada 24 horas. Es completamente seguro y muy รบtil para los desarrolladores." - }, - "settingsSaved": "Ajustes guardados", - "settingsSavedDetail": "ยกLos ajustes se guardaron correctamente!", - "settingsError": "Hubo un error al guardar los ajustes." + "title": "Preferencias globales", + "description": "Configura los ajustes globales de tu instancia de Booklore, incluyendo el manejo de imรกgenes de portada, preferencias de bรบsqueda y lรญmites de carga de archivos.", + "covers": { + "sectionTitle": "Imagen de portada del libro", + "regenerate": "Regenerar portadas", + "regenerateAllBtn": "Regenerar todas", + "regenerateMissingBtn": "Regenerar faltantes", + "regenerateDesc": "Regenera las imรกgenes de portada de todos los libros a partir de las portadas incrustadas en el archivo. Usa \"Regenerar faltantes\" para generar portadas solo para libros que aรบn no tienen una.", + "regenerateStarted": "Regeneraciรณn de portadas iniciada", + "regenerateStartedDetail": "Las portadas de los libros se estรกn regenerando.", + "regenerateError": "Error al iniciar la regeneraciรณn de portadas.", + "verticalCropping": "Recorte vertical de portada", + "verticalCroppingDesc": "Recortar automรกticamente imรกgenes extremadamente altas (como webcรณmics) desde la parte superior para crear miniaturas de portada utilizables.", + "horizontalCropping": "Recorte horizontal de portada", + "horizontalCroppingDesc": "Recortar automรกticamente imรกgenes extremadamente anchas desde la izquierda para crear miniaturas de portada utilizables.", + "aspectRatio": "Umbral de relaciรณn de aspecto: {{value}}", + "aspectRatioDesc": "Las imรกgenes con relaciones de aspecto que excedan este umbral serรกn recortadas. Un valor de 2.5 significa que las imรกgenes mรกs de 2.5 veces mรกs altas (o anchas) de lo normal serรกn recortadas.", + "smartCropping": "Recorte inteligente", + "smartCroppingDesc": "Omitir regiones de color uniforme al determinar dรณnde recortar. Enfoca la imagen de portada en el contenido mรกs relevante." + }, + "search": { + "sectionTitle": "Bรบsqueda y recomendaciones", + "autoBookSearch": "Bรบsqueda automรกtica de libros", + "autoBookSearchDesc": "Intenta automรกticamente emparejar metadatos cuando se abre el panel de informaciรณn del libro.", + "similarBook": "Recomendaciรณn de libros similares", + "similarBookDesc": "Habilita o deshabilita las recomendaciones de libros similares basadas en tu biblioteca." + }, + "fileManagement": { + "sectionTitle": "Gestiรณn de archivos", + "maxUploadSize": "Tamaรฑo mรกximo de carga de archivo", + "maxUploadPlaceholder": "Tamaรฑo mรกximo", + "maxUploadDesc": "Define el tamaรฑo mรกximo permitido (en MB) para cada archivo cargado. Se aplica a todos los formatos de archivo compatibles.", + "restartWarning": "Los cambios surtirรกn efecto despuรฉs de reiniciar el servidor", + "invalidInput": "Entrada invรกlida", + "invalidInputDetail": "Introduce un tamaรฑo mรกximo de carga de archivo vรกlido en MB." + }, + "appearance": { + "sectionTitle": "Apariencia", + "supportButtonAnimation": "Animaciรณn del botรณn de apoyo", + "supportButtonAnimationDesc": "Muestra el efecto animado de corazรณn en el botรณn de apoyo en la barra superior. Desactivar esto mantiene el botรณn visible pero elimina la animaciรณn." + }, + "settingsSaved": "Ajustes guardados", + "settingsSavedDetail": "ยกLos ajustes se guardaron correctamente!", + "settingsError": "Hubo un error al guardar los ajustes." } diff --git a/booklore-ui/src/i18n/fr/settings-application.json b/booklore-ui/src/i18n/fr/settings-application.json index 69c5d6413..75d28d40d 100644 --- a/booklore-ui/src/i18n/fr/settings-application.json +++ b/booklore-ui/src/i18n/fr/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Prรฉfรฉrences globales", - "description": "Configurez les paramรจtres globaux de votre instance Booklore, y compris la gestion des images de couverture, les prรฉfรฉrences de recherche et les limites de tรฉlรฉversement de fichiers.", - "covers": { - "sectionTitle": "Image de couverture des livres", - "regenerate": "Rรฉgรฉnรฉrer les couvertures", - "regenerateBtn": "Rรฉgรฉnรฉrer", - "regenerateDesc": "Rรฉgรฉnรจre les images de couverture pour tous les livres ร  partir des couvertures intรฉgrรฉes dans le fichier.", - "regenerateStarted": "Rรฉgรฉnรฉration des couvertures lancรฉe", - "regenerateStartedDetail": "Les couvertures des livres sont en cours de rรฉgรฉnรฉration.", - "regenerateError": "ร‰chec du lancement de la rรฉgรฉnรฉration des couvertures.", - "verticalCropping": "Recadrage vertical des couvertures", - "verticalCroppingDesc": "Recadrer automatiquement les images extrรชmement hautes (comme les webcomics) depuis le haut pour crรฉer des miniatures de couverture utilisables.", - "horizontalCropping": "Recadrage horizontal des couvertures", - "horizontalCroppingDesc": "Recadrer automatiquement les images extrรชmement larges depuis la gauche pour crรฉer des miniatures de couverture utilisables.", - "aspectRatio": "Seuil de rapport d'aspect : {{value}}", - "aspectRatioDesc": "Les images dont le rapport d'aspect dรฉpasse ce seuil seront recadrรฉes. Une valeur de 2,5 signifie que les images plus de 2,5 fois plus hautes (ou plus larges) que la normale seront recadrรฉes.", - "smartCropping": "Recadrage intelligent", - "smartCroppingDesc": "Ignorer les rรฉgions de couleur uniforme lors de la dรฉtermination de l'endroit oรน recadrer. Concentre l'image de couverture sur le contenu le plus pertinent." - }, - "search": { - "sectionTitle": "Recherche et recommandations", - "autoBookSearch": "Recherche automatique de livres", - "autoBookSearchDesc": "Tente automatiquement la correspondance des mรฉtadonnรฉes lorsque le panneau d'informations du livre est ouvert.", - "similarBook": "Recommandation de livres similaires", - "similarBookDesc": "Active ou dรฉsactive les recommandations de livres similaires basรฉes sur votre bibliothรจque." - }, - "fileManagement": { - "sectionTitle": "Gestion des fichiers", - "maxUploadSize": "Taille max. de tรฉlรฉversement", - "maxUploadPlaceholder": "Taille max.", - "maxUploadDesc": "Dรฉfinit la taille maximale autorisรฉe (en Mo) pour chaque fichier tรฉlรฉversรฉ. S'applique ร  tous les formats supportรฉs.", - "restartWarning": "Les modifications prendront effet aprรจs le redรฉmarrage du serveur", - "invalidInput": "Entrรฉe invalide", - "invalidInputDetail": "Veuillez entrer une taille maximale de tรฉlรฉversement valide en Mo." - }, - "telemetry": { - "sectionTitle": "Tรฉlรฉmรฉtrie", - "enableTelemetry": "Activer la tรฉlรฉmรฉtrie", - "telemetryDesc": "Aidez ร  amรฉliorer Booklore en partageant des statistiques d'utilisation anonymes. Ces donnรฉes permettent aux dรฉveloppeurs de voir quelles fonctionnalitรฉs sont les plus utilisรฉes, d'identifier les bugs et de repรฉrer les problรจmes de performance afin de savoir sur quoi travailler ensuite. Aucune information personnelle, contenu de livre ou donnรฉe identifiable n'est jamais envoyรฉ. Les donnรฉes sont envoyรฉes automatiquement au serveur Booklore une fois toutes les 24 heures. C'est totalement sรปr et trรจs utile pour les dรฉveloppeurs." - }, - "settingsSaved": "Paramรจtres enregistrรฉs", - "settingsSavedDetail": "Les paramรจtres ont รฉtรฉ enregistrรฉs avec succรจs !", - "settingsError": "Une erreur s'est produite lors de l'enregistrement des paramรจtres.", - "appearance": { - "supportButtonAnimation": "Animation du bouton de support", - "supportButtonAnimationDesc": "", - "sectionTitle": "Apparence" - } + "title": "Prรฉfรฉrences globales", + "description": "Configurez les paramรจtres globaux de votre instance Booklore, y compris la gestion des images de couverture, les prรฉfรฉrences de recherche et les limites de tรฉlรฉversement de fichiers.", + "covers": { + "sectionTitle": "Image de couverture des livres", + "regenerate": "Rรฉgรฉnรฉrer les couvertures", + "regenerateBtn": "Rรฉgรฉnรฉrer", + "regenerateDesc": "Rรฉgรฉnรจre les images de couverture pour tous les livres ร  partir des couvertures intรฉgrรฉes dans le fichier.", + "regenerateStarted": "Rรฉgรฉnรฉration des couvertures lancรฉe", + "regenerateStartedDetail": "Les couvertures des livres sont en cours de rรฉgรฉnรฉration.", + "regenerateError": "ร‰chec du lancement de la rรฉgรฉnรฉration des couvertures.", + "verticalCropping": "Recadrage vertical des couvertures", + "verticalCroppingDesc": "Recadrer automatiquement les images extrรชmement hautes (comme les webcomics) depuis le haut pour crรฉer des miniatures de couverture utilisables.", + "horizontalCropping": "Recadrage horizontal des couvertures", + "horizontalCroppingDesc": "Recadrer automatiquement les images extrรชmement larges depuis la gauche pour crรฉer des miniatures de couverture utilisables.", + "aspectRatio": "Seuil de rapport d'aspect : {{value}}", + "aspectRatioDesc": "Les images dont le rapport d'aspect dรฉpasse ce seuil seront recadrรฉes. Une valeur de 2,5 signifie que les images plus de 2,5 fois plus hautes (ou plus larges) que la normale seront recadrรฉes.", + "smartCropping": "Recadrage intelligent", + "smartCroppingDesc": "Ignorer les rรฉgions de couleur uniforme lors de la dรฉtermination de l'endroit oรน recadrer. Concentre l'image de couverture sur le contenu le plus pertinent." + }, + "search": { + "sectionTitle": "Recherche et recommandations", + "autoBookSearch": "Recherche automatique de livres", + "autoBookSearchDesc": "Tente automatiquement la correspondance des mรฉtadonnรฉes lorsque le panneau d'informations du livre est ouvert.", + "similarBook": "Recommandation de livres similaires", + "similarBookDesc": "Active ou dรฉsactive les recommandations de livres similaires basรฉes sur votre bibliothรจque." + }, + "fileManagement": { + "sectionTitle": "Gestion des fichiers", + "maxUploadSize": "Taille max. de tรฉlรฉversement", + "maxUploadPlaceholder": "Taille max.", + "maxUploadDesc": "Dรฉfinit la taille maximale autorisรฉe (en Mo) pour chaque fichier tรฉlรฉversรฉ. S'applique ร  tous les formats supportรฉs.", + "restartWarning": "Les modifications prendront effet aprรจs le redรฉmarrage du serveur", + "invalidInput": "Entrรฉe invalide", + "invalidInputDetail": "Veuillez entrer une taille maximale de tรฉlรฉversement valide en Mo." + }, + "settingsSaved": "Paramรจtres enregistrรฉs", + "settingsSavedDetail": "Les paramรจtres ont รฉtรฉ enregistrรฉs avec succรจs !", + "settingsError": "Une erreur s'est produite lors de l'enregistrement des paramรจtres.", + "appearance": { + "supportButtonAnimation": "Animation du bouton de support", + "supportButtonAnimationDesc": "", + "sectionTitle": "Apparence" + } } diff --git a/booklore-ui/src/i18n/hr/settings-application.json b/booklore-ui/src/i18n/hr/settings-application.json index d1b1c1d40..11c200e8a 100644 --- a/booklore-ui/src/i18n/hr/settings-application.json +++ b/booklore-ui/src/i18n/hr/settings-application.json @@ -1,52 +1,47 @@ { - "title": "Globalne postavke", - "description": "Konfigurirajte globalne postavke za vaลกu Booklore instancu, ukljuฤujuฤ‡i rukovanje naslovnicama, preferencije pretraลพivanja i ograniฤenja uฤitavanja datoteka.", - "covers": { - "sectionTitle": "Naslovna slika knjige", - "regenerate": "Regeneriraj naslovnice", - "regenerateBtn": "Regeneriraj", - "regenerateDesc": "Obnavlja slike naslovnica za sve knjige iz ugraฤ‘enih naslovnica u datoteci. Koristite \"Obnovi nedostajuฤ‡e\" za generiranje naslovnica samo za knjige koje joลก nemaju naslovnicu.", - "regenerateStarted": "Regeneracija naslovnica pokrenuta", - "regenerateStartedDetail": "Naslovnice knjiga se regeneriraju.", - "regenerateError": "Pokretanje regeneracije naslovnica nije uspjelo.", - "verticalCropping": "Vertikalno izrezivanje naslovnica", - "verticalCroppingDesc": "Automatski izrezuje iznimno visoke slike (poput web stripova) od vrha kako bi se stvorile upotrebljive minijature naslovnica.", - "horizontalCropping": "Horizontalno izrezivanje naslovnica", - "horizontalCroppingDesc": "Automatski izrezuje iznimno ลกiroke slike s lijeve strane kako bi se stvorile upotrebljive minijature naslovnica.", - "aspectRatio": "Prag omjera slike: {{value}}", - "aspectRatioDesc": "Slike s omjerima koji premaลกuju ovaj prag bit ฤ‡e izrezane. Vrijednost 2,5 znaฤi da ฤ‡e slike koje su viลกe od 2,5x viลกe (ili ลกire) od normalnog biti izrezane.", - "smartCropping": "Pametno izrezivanje", - "smartCroppingDesc": "Preskoฤi jednobojne regije pri odreฤ‘ivanju mjesta izrezivanja. Fokusira naslovnicu na najrelevantniji sadrลพaj.", - "regenerateAllBtn": "Obnovi sve", - "regenerateMissingBtn": "Obnovi nedostajuฤ‡e" - }, - "search": { - "sectionTitle": "Pretraลพivanje i preporuke", - "autoBookSearch": "Automatsko pretraลพivanje knjiga", - "autoBookSearchDesc": "Automatski pokuลกava pronaฤ‡i metapodatke kada se otvori panel informacija o knjizi.", - "similarBook": "Preporuka sliฤnih knjiga", - "similarBookDesc": "Omoguฤ‡uje ili onemoguฤ‡uje preporuke sliฤnih knjiga na temelju vaลกe zbirke." - }, - "fileManagement": { - "sectionTitle": "Upravljanje datotekama", - "maxUploadSize": "Maks. veliฤina uฤitavanja", - "maxUploadPlaceholder": "Maks. veliฤina", - "maxUploadDesc": "Definira maksimalnu dopuลกtenu veliฤinu (u MB) za svaku prenesenu datoteku. Odnosi se na sve podrลพane formate datoteka.", - "restartWarning": "Promjene ฤ‡e stupiti na snagu nakon ponovnog pokretanja posluลพitelja", - "invalidInput": "Nevaลพeฤ‡i unos", - "invalidInputDetail": "Unesite valjanu maksimalnu veliฤinu uฤitavanja datoteke u MB." - }, - "telemetry": { - "sectionTitle": "Telemetrija", - "enableTelemetry": "Omoguฤ‡i telemetriju", - "telemetryDesc": "Pomozite poboljลกati Booklore dijeljenjem anonimne statistike koriลกtenja. Ovi podaci omoguฤ‡uju programerima da vide koje se znaฤajke najviลกe koriste, identificiraju bugove i uoฤe probleme s performansama kako bi znali na ฤemu dalje raditi. Nikada se ne ลกalju osobni podaci, sadrลพaj knjiga ili bilo koji identifikacijski podaci. Podaci se automatski ลกalju na Booklore posluลพitelj jednom u 24 sata. Potpuno je sigurno i vrlo korisno za programere." - }, - "settingsSaved": "Postavke spremljene", - "settingsSavedDetail": "Postavke su uspjeลกno spremljene!", - "settingsError": "Doลกlo je do pogreลกke prilikom spremanja postavki.", - "appearance": { - "supportButtonAnimation": "Animacija gumba za podrลกku", - "supportButtonAnimationDesc": "Prikaลพi animirani efekt srca na gumbu za podrลกku u gornjoj traci. Iskljuฤivanjem ove opcije gumb ostaje vidljiv, ali se animacija uklanja.", - "sectionTitle": "Izgled" - } + "title": "Globalne postavke", + "description": "Konfigurirajte globalne postavke za vaลกu Booklore instancu, ukljuฤujuฤ‡i rukovanje naslovnicama, preferencije pretraลพivanja i ograniฤenja uฤitavanja datoteka.", + "covers": { + "sectionTitle": "Naslovna slika knjige", + "regenerate": "Regeneriraj naslovnice", + "regenerateBtn": "Regeneriraj", + "regenerateDesc": "Obnavlja slike naslovnica za sve knjige iz ugraฤ‘enih naslovnica u datoteci. Koristite \"Obnovi nedostajuฤ‡e\" za generiranje naslovnica samo za knjige koje joลก nemaju naslovnicu.", + "regenerateStarted": "Regeneracija naslovnica pokrenuta", + "regenerateStartedDetail": "Naslovnice knjiga se regeneriraju.", + "regenerateError": "Pokretanje regeneracije naslovnica nije uspjelo.", + "verticalCropping": "Vertikalno izrezivanje naslovnica", + "verticalCroppingDesc": "Automatski izrezuje iznimno visoke slike (poput web stripova) od vrha kako bi se stvorile upotrebljive minijature naslovnica.", + "horizontalCropping": "Horizontalno izrezivanje naslovnica", + "horizontalCroppingDesc": "Automatski izrezuje iznimno ลกiroke slike s lijeve strane kako bi se stvorile upotrebljive minijature naslovnica.", + "aspectRatio": "Prag omjera slike: {{value}}", + "aspectRatioDesc": "Slike s omjerima koji premaลกuju ovaj prag bit ฤ‡e izrezane. Vrijednost 2,5 znaฤi da ฤ‡e slike koje su viลกe od 2,5x viลกe (ili ลกire) od normalnog biti izrezane.", + "smartCropping": "Pametno izrezivanje", + "smartCroppingDesc": "Preskoฤi jednobojne regije pri odreฤ‘ivanju mjesta izrezivanja. Fokusira naslovnicu na najrelevantniji sadrลพaj.", + "regenerateAllBtn": "Obnovi sve", + "regenerateMissingBtn": "Obnovi nedostajuฤ‡e" + }, + "search": { + "sectionTitle": "Pretraลพivanje i preporuke", + "autoBookSearch": "Automatsko pretraลพivanje knjiga", + "autoBookSearchDesc": "Automatski pokuลกava pronaฤ‡i metapodatke kada se otvori panel informacija o knjizi.", + "similarBook": "Preporuka sliฤnih knjiga", + "similarBookDesc": "Omoguฤ‡uje ili onemoguฤ‡uje preporuke sliฤnih knjiga na temelju vaลกe zbirke." + }, + "fileManagement": { + "sectionTitle": "Upravljanje datotekama", + "maxUploadSize": "Maks. veliฤina uฤitavanja", + "maxUploadPlaceholder": "Maks. veliฤina", + "maxUploadDesc": "Definira maksimalnu dopuลกtenu veliฤinu (u MB) za svaku prenesenu datoteku. Odnosi se na sve podrลพane formate datoteka.", + "restartWarning": "Promjene ฤ‡e stupiti na snagu nakon ponovnog pokretanja posluลพitelja", + "invalidInput": "Nevaลพeฤ‡i unos", + "invalidInputDetail": "Unesite valjanu maksimalnu veliฤinu uฤitavanja datoteke u MB." + }, + "settingsSaved": "Postavke spremljene", + "settingsSavedDetail": "Postavke su uspjeลกno spremljene!", + "settingsError": "Doลกlo je do pogreลกke prilikom spremanja postavki.", + "appearance": { + "supportButtonAnimation": "Animacija gumba za podrลกku", + "supportButtonAnimationDesc": "Prikaลพi animirani efekt srca na gumbu za podrลกku u gornjoj traci. Iskljuฤivanjem ove opcije gumb ostaje vidljiv, ali se animacija uklanja.", + "sectionTitle": "Izgled" + } } diff --git a/booklore-ui/src/i18n/hu/settings-application.json b/booklore-ui/src/i18n/hu/settings-application.json index b3388632d..a6f613f50 100644 --- a/booklore-ui/src/i18n/hu/settings-application.json +++ b/booklore-ui/src/i18n/hu/settings-application.json @@ -1,50 +1,45 @@ { - "title": "", - "description": "", - "covers": { - "sectionTitle": "", - "regenerate": "", - "regenerateBtn": "", - "regenerateDesc": "", - "regenerateStarted": "", - "regenerateStartedDetail": "", - "regenerateError": "", - "verticalCropping": "", - "verticalCroppingDesc": "", - "horizontalCropping": "", - "horizontalCroppingDesc": "", - "aspectRatio": "", - "aspectRatioDesc": "", - "smartCropping": "", - "smartCroppingDesc": "" - }, - "search": { - "sectionTitle": "", - "autoBookSearch": "", - "autoBookSearchDesc": "", - "similarBook": "", - "similarBookDesc": "" - }, - "fileManagement": { - "sectionTitle": "", - "maxUploadSize": "", - "maxUploadPlaceholder": "", - "maxUploadDesc": "", - "restartWarning": "", - "invalidInput": "", - "invalidInputDetail": "" - }, - "telemetry": { - "sectionTitle": "", - "enableTelemetry": "", - "telemetryDesc": "" - }, - "settingsSaved": "", - "settingsSavedDetail": "", - "settingsError": "", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "", + "description": "", + "covers": { + "sectionTitle": "", + "regenerate": "", + "regenerateBtn": "", + "regenerateDesc": "", + "regenerateStarted": "", + "regenerateStartedDetail": "", + "regenerateError": "", + "verticalCropping": "", + "verticalCroppingDesc": "", + "horizontalCropping": "", + "horizontalCroppingDesc": "", + "aspectRatio": "", + "aspectRatioDesc": "", + "smartCropping": "", + "smartCroppingDesc": "" + }, + "search": { + "sectionTitle": "", + "autoBookSearch": "", + "autoBookSearchDesc": "", + "similarBook": "", + "similarBookDesc": "" + }, + "fileManagement": { + "sectionTitle": "", + "maxUploadSize": "", + "maxUploadPlaceholder": "", + "maxUploadDesc": "", + "restartWarning": "", + "invalidInput": "", + "invalidInputDetail": "" + }, + "settingsSaved": "", + "settingsSavedDetail": "", + "settingsError": "", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/id/settings-application.json b/booklore-ui/src/i18n/id/settings-application.json index a76ee5f5f..25b90258a 100644 --- a/booklore-ui/src/i18n/id/settings-application.json +++ b/booklore-ui/src/i18n/id/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Preferensi Global", - "description": "", - "covers": { - "sectionTitle": "Gambar Sampul Buku", - "regenerate": "", - "regenerateBtn": "", - "regenerateDesc": "", - "regenerateStarted": "", - "regenerateStartedDetail": "", - "regenerateError": "", - "verticalCropping": "", - "verticalCroppingDesc": "", - "horizontalCropping": "", - "horizontalCroppingDesc": "", - "aspectRatio": "", - "aspectRatioDesc": "", - "smartCropping": "", - "smartCroppingDesc": "" - }, - "search": { - "sectionTitle": "", - "autoBookSearch": "", - "autoBookSearchDesc": "", - "similarBook": "", - "similarBookDesc": "" - }, - "fileManagement": { - "sectionTitle": "", - "maxUploadSize": "", - "maxUploadPlaceholder": "", - "maxUploadDesc": "", - "restartWarning": "", - "invalidInput": "", - "invalidInputDetail": "" - }, - "appearance": { - "sectionTitle": "", - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "" - }, - "telemetry": { - "sectionTitle": "", - "enableTelemetry": "", - "telemetryDesc": "" - }, - "settingsSaved": "", - "settingsSavedDetail": "", - "settingsError": "" + "title": "Preferensi Global", + "description": "", + "covers": { + "sectionTitle": "Gambar Sampul Buku", + "regenerate": "", + "regenerateBtn": "", + "regenerateDesc": "", + "regenerateStarted": "", + "regenerateStartedDetail": "", + "regenerateError": "", + "verticalCropping": "", + "verticalCroppingDesc": "", + "horizontalCropping": "", + "horizontalCroppingDesc": "", + "aspectRatio": "", + "aspectRatioDesc": "", + "smartCropping": "", + "smartCroppingDesc": "" + }, + "search": { + "sectionTitle": "", + "autoBookSearch": "", + "autoBookSearchDesc": "", + "similarBook": "", + "similarBookDesc": "" + }, + "fileManagement": { + "sectionTitle": "", + "maxUploadSize": "", + "maxUploadPlaceholder": "", + "maxUploadDesc": "", + "restartWarning": "", + "invalidInput": "", + "invalidInputDetail": "" + }, + "appearance": { + "sectionTitle": "", + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "" + }, + "settingsSaved": "", + "settingsSavedDetail": "", + "settingsError": "" } diff --git a/booklore-ui/src/i18n/it/settings-application.json b/booklore-ui/src/i18n/it/settings-application.json index 6626768a7..d1e36d2d3 100644 --- a/booklore-ui/src/i18n/it/settings-application.json +++ b/booklore-ui/src/i18n/it/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Preferenze globali", - "description": "Configura le impostazioni globali per la tua istanza di Booklore, inclusa la gestione delle immagini di copertina, le preferenze di ricerca e i limiti di caricamento file.", - "covers": { - "sectionTitle": "Immagine di copertina", - "regenerate": "Rigenera copertine", - "regenerateBtn": "Rigenera", - "regenerateDesc": "Rigenera le immagini di copertina per tutti i libri dalle copertine incorporate nei file.", - "regenerateStarted": "Rigenerazione copertine avviata", - "regenerateStartedDetail": "Le copertine dei libri sono in fase di rigenerazione.", - "regenerateError": "Impossibile avviare la rigenerazione delle copertine.", - "verticalCropping": "Ritaglio verticale copertina", - "verticalCroppingDesc": "Ritaglia automaticamente le immagini estremamente alte (come i web comic) dall'alto per creare miniature di copertina utilizzabili.", - "horizontalCropping": "Ritaglio orizzontale copertina", - "horizontalCroppingDesc": "Ritaglia automaticamente le immagini estremamente larghe da sinistra per creare miniature di copertina utilizzabili.", - "aspectRatio": "Soglia rapporto d'aspetto: {{value}}", - "aspectRatioDesc": "Le immagini con rapporti d'aspetto superiori a questa soglia verranno ritagliate. Un valore di 2.5 significa che le immagini piรน di 2.5 volte piรน alte (o piรน larghe) del normale verranno ritagliate.", - "smartCropping": "Ritaglio intelligente", - "smartCroppingDesc": "Ignora le regioni di colore uniforme quando si determina dove ritagliare. Focalizza l'immagine di copertina sul contenuto piรน rilevante." - }, - "search": { - "sectionTitle": "Ricerca e raccomandazioni", - "autoBookSearch": "Ricerca automatica libri", - "autoBookSearchDesc": "Tenta automaticamente la corrispondenza dei metadati quando viene aperto il pannello informazioni del libro.", - "similarBook": "Raccomandazione libri simili", - "similarBookDesc": "Abilita o disabilita le raccomandazioni di libri simili basate sulla tua libreria." - }, - "fileManagement": { - "sectionTitle": "Gestione file", - "maxUploadSize": "Dimensione massima caricamento file", - "maxUploadPlaceholder": "Dimensione massima", - "maxUploadDesc": "Definisce la dimensione massima consentita (in MB) per ogni file caricato. Si applica ai formati EPUB, PDF, CBZ, CBR e CB7.", - "restartWarning": "Le modifiche avranno effetto dopo il riavvio del server", - "invalidInput": "Input non valido", - "invalidInputDetail": "Inserisci una dimensione massima valida per il caricamento file in MB." - }, - "telemetry": { - "sectionTitle": "Telemetria", - "enableTelemetry": "Abilita telemetria", - "telemetryDesc": "Aiuta a migliorare Booklore condividendo statistiche d'uso anonime. Questi dati permettono agli sviluppatori di vedere quali funzionalitร  sono piรน utilizzate, identificare bug e individuare problemi di prestazioni per sapere su cosa lavorare. Nessuna informazione personale, contenuto dei libri o dato identificabile viene mai inviato. I dati vengono inviati automaticamente al server Booklore una volta ogni 24 ore. รˆ completamente sicuro e molto utile per gli sviluppatori." - }, - "settingsSaved": "Impostazioni salvate", - "settingsSavedDetail": "Le impostazioni sono state salvate con successo!", - "settingsError": "Si รจ verificato un errore durante il salvataggio delle impostazioni.", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "Preferenze globali", + "description": "Configura le impostazioni globali per la tua istanza di Booklore, inclusa la gestione delle immagini di copertina, le preferenze di ricerca e i limiti di caricamento file.", + "covers": { + "sectionTitle": "Immagine di copertina", + "regenerate": "Rigenera copertine", + "regenerateBtn": "Rigenera", + "regenerateDesc": "Rigenera le immagini di copertina per tutti i libri dalle copertine incorporate nei file.", + "regenerateStarted": "Rigenerazione copertine avviata", + "regenerateStartedDetail": "Le copertine dei libri sono in fase di rigenerazione.", + "regenerateError": "Impossibile avviare la rigenerazione delle copertine.", + "verticalCropping": "Ritaglio verticale copertina", + "verticalCroppingDesc": "Ritaglia automaticamente le immagini estremamente alte (come i web comic) dall'alto per creare miniature di copertina utilizzabili.", + "horizontalCropping": "Ritaglio orizzontale copertina", + "horizontalCroppingDesc": "Ritaglia automaticamente le immagini estremamente larghe da sinistra per creare miniature di copertina utilizzabili.", + "aspectRatio": "Soglia rapporto d'aspetto: {{value}}", + "aspectRatioDesc": "Le immagini con rapporti d'aspetto superiori a questa soglia verranno ritagliate. Un valore di 2.5 significa che le immagini piรน di 2.5 volte piรน alte (o piรน larghe) del normale verranno ritagliate.", + "smartCropping": "Ritaglio intelligente", + "smartCroppingDesc": "Ignora le regioni di colore uniforme quando si determina dove ritagliare. Focalizza l'immagine di copertina sul contenuto piรน rilevante." + }, + "search": { + "sectionTitle": "Ricerca e raccomandazioni", + "autoBookSearch": "Ricerca automatica libri", + "autoBookSearchDesc": "Tenta automaticamente la corrispondenza dei metadati quando viene aperto il pannello informazioni del libro.", + "similarBook": "Raccomandazione libri simili", + "similarBookDesc": "Abilita o disabilita le raccomandazioni di libri simili basate sulla tua libreria." + }, + "fileManagement": { + "sectionTitle": "Gestione file", + "maxUploadSize": "Dimensione massima caricamento file", + "maxUploadPlaceholder": "Dimensione massima", + "maxUploadDesc": "Definisce la dimensione massima consentita (in MB) per ogni file caricato. Si applica ai formati EPUB, PDF, CBZ, CBR e CB7.", + "restartWarning": "Le modifiche avranno effetto dopo il riavvio del server", + "invalidInput": "Input non valido", + "invalidInputDetail": "Inserisci una dimensione massima valida per il caricamento file in MB." + }, + "settingsSaved": "Impostazioni salvate", + "settingsSavedDetail": "Le impostazioni sono state salvate con successo!", + "settingsError": "Si รจ verificato un errore durante il salvataggio delle impostazioni.", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/ja/settings-application.json b/booklore-ui/src/i18n/ja/settings-application.json index afce47554..2c76e0f8b 100644 --- a/booklore-ui/src/i18n/ja/settings-application.json +++ b/booklore-ui/src/i18n/ja/settings-application.json @@ -1,50 +1,45 @@ { - "title": "ใ‚ฐใƒญใƒผใƒใƒซ่จญๅฎš", - "description": "ใ‚ซใƒใƒผ็”ปๅƒใฎๅ‡ฆ็†ใ€ๆคœ็ดข่จญๅฎšใ€ใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ๅˆถ้™ใชใฉใ€Bookloreใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใฎใ‚ฐใƒญใƒผใƒใƒซ่จญๅฎšใ‚’ๆง‹ๆˆใ—ใพใ™ใ€‚", - "covers": { - "sectionTitle": "ใƒ–ใƒƒใ‚ฏใ‚ซใƒใƒผ็”ปๅƒ", - "regenerate": "ใ‚ซใƒใƒผใ‚’ๅ†็”Ÿๆˆ", - "regenerateBtn": "ๅ†็”Ÿๆˆ", - "regenerateDesc": "ใ™ในใฆใฎๆœฌใฎใ‚ซใƒใƒผ็”ปๅƒใ‚’ใƒ•ใ‚กใ‚คใƒซใซๅŸ‹ใ‚่พผใพใ‚ŒใŸใ‚ซใƒใƒผใ‹ใ‚‰ๅ†็”Ÿๆˆใ—ใพใ™ใ€‚", - "regenerateStarted": "ใ‚ซใƒใƒผๅ†็”Ÿๆˆใ‚’้–‹ๅง‹ใ—ใพใ—ใŸ", - "regenerateStartedDetail": "ใƒ–ใƒƒใ‚ฏใ‚ซใƒใƒผใ‚’ๅ†็”Ÿๆˆไธญใงใ™ใ€‚", - "regenerateError": "ใ‚ซใƒใƒผๅ†็”Ÿๆˆใฎ้–‹ๅง‹ใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚", - "verticalCropping": "็ธฆๆ–นๅ‘ใ‚ซใƒใƒผใƒˆใƒชใƒŸใƒณใ‚ฐ", - "verticalCroppingDesc": "้žๅธธใซ็ธฆ้•ทใฎ็”ปๅƒ๏ผˆWebใ‚ณใƒŸใƒƒใ‚ฏใชใฉ๏ผ‰ใ‚’ไธŠ้ƒจใ‹ใ‚‰่‡ชๅ‹•็š„ใซใƒˆใƒชใƒŸใƒณใ‚ฐใ—ใฆใ€ไฝฟใ„ใ‚„ใ™ใ„ใ‚ซใƒใƒผใ‚ตใƒ ใƒใ‚คใƒซใ‚’ไฝœๆˆใ—ใพใ™ใ€‚", - "horizontalCropping": "ๆจชๆ–นๅ‘ใ‚ซใƒใƒผใƒˆใƒชใƒŸใƒณใ‚ฐ", - "horizontalCroppingDesc": "้žๅธธใซๆจช้•ทใฎ็”ปๅƒใ‚’ๅทฆๅดใ‹ใ‚‰่‡ชๅ‹•็š„ใซใƒˆใƒชใƒŸใƒณใ‚ฐใ—ใฆใ€ไฝฟใ„ใ‚„ใ™ใ„ใ‚ซใƒใƒผใ‚ตใƒ ใƒใ‚คใƒซใ‚’ไฝœๆˆใ—ใพใ™ใ€‚", - "aspectRatio": "ใ‚ขใ‚นใƒšใ‚ฏใƒˆๆฏ”ใ—ใใ„ๅ€ค: {{value}}", - "aspectRatioDesc": "ใ“ใฎใ—ใใ„ๅ€คใ‚’่ถ…ใˆใ‚‹ใ‚ขใ‚นใƒšใ‚ฏใƒˆๆฏ”ใฎ็”ปๅƒใŒใƒˆใƒชใƒŸใƒณใ‚ฐใ•ใ‚Œใพใ™ใ€‚2.5ใฎๅ€คใฏใ€้€šๅธธใฎ2.5ๅ€ไปฅไธŠ็ธฆ้•ท๏ผˆใพใŸใฏๆจช้•ท๏ผ‰ใฎ็”ปๅƒใŒใƒˆใƒชใƒŸใƒณใ‚ฐใ•ใ‚Œใ‚‹ใ“ใจใ‚’ๆ„ๅ‘ณใ—ใพใ™ใ€‚", - "smartCropping": "ใ‚นใƒžใƒผใƒˆใƒˆใƒชใƒŸใƒณใ‚ฐ", - "smartCroppingDesc": "ใƒˆใƒชใƒŸใƒณใ‚ฐไฝ็ฝฎใ‚’ๆฑบๅฎšใ™ใ‚‹้š›ใซๅ‡ไธ€ใช่‰ฒใฎ้ ˜ๅŸŸใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™ใ€‚ใ‚ซใƒใƒผ็”ปๅƒใ‚’ๆœ€ใ‚‚้–ข้€ฃๆ€งใฎ้ซ˜ใ„ใ‚ณใƒณใƒ†ใƒณใƒ„ใซ็„ฆ็‚นใ‚’ๅฝ“ใฆใพใ™ใ€‚" - }, - "search": { - "sectionTitle": "ๆคœ็ดขใจใŠใ™ใ™ใ‚", - "autoBookSearch": "่‡ชๅ‹•ใƒ–ใƒƒใ‚ฏๆคœ็ดข", - "autoBookSearchDesc": "ๆœฌใฎๆƒ…ๅ ฑใƒ‘ใƒใƒซใ‚’้–‹ใ„ใŸใจใใซใ€่‡ชๅ‹•็š„ใซใƒกใ‚ฟใƒ‡ใƒผใ‚ฟใƒžใƒƒใƒใƒณใ‚ฐใ‚’่ฉฆใฟใพใ™ใ€‚", - "similarBook": "้กžไผผๆœฌใฎใŠใ™ใ™ใ‚", - "similarBookDesc": "ใƒฉใ‚คใƒ–ใƒฉใƒชใซๅŸบใฅใ„ใŸ้กžไผผๆœฌใฎใŠใ™ใ™ใ‚ใ‚’ๆœ‰ๅŠนใพใŸใฏ็„กๅŠนใซใ—ใพใ™ใ€‚" - }, - "fileManagement": { - "sectionTitle": "ใƒ•ใ‚กใ‚คใƒซ็ฎก็†", - "maxUploadSize": "ๆœ€ๅคงใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚ตใ‚คใ‚บ", - "maxUploadPlaceholder": "ๆœ€ๅคงใ‚ตใ‚คใ‚บ", - "maxUploadDesc": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใƒ•ใ‚กใ‚คใƒซใฎๆœ€ๅคง่จฑๅฎนใ‚ตใ‚คใ‚บ๏ผˆMBๅ˜ไฝ๏ผ‰ใ‚’ๅฎš็พฉใ—ใพใ™ใ€‚EPUBใ€PDFใ€CBZใ€CBRใ€CB7ๅฝขๅผใซ้ฉ็”จใ•ใ‚Œใพใ™ใ€‚", - "restartWarning": "ๅค‰ๆ›ดใฏใ‚ตใƒผใƒใƒผใฎๅ†่ตทๅ‹•ๅพŒใซๆœ‰ๅŠนใซใชใ‚Šใพใ™", - "invalidInput": "็„กๅŠนใชๅ…ฅๅŠ›", - "invalidInputDetail": "ๆœ‰ๅŠนใชๆœ€ๅคงใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚ตใ‚คใ‚บ๏ผˆMB๏ผ‰ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚" - }, - "telemetry": { - "sectionTitle": "ใƒ†ใƒฌใƒกใƒˆใƒช", - "enableTelemetry": "ใƒ†ใƒฌใƒกใƒˆใƒชใ‚’ๆœ‰ๅŠนๅŒ–", - "telemetryDesc": "ๅŒฟๅใฎไฝฟ็”จ็ตฑ่จˆใ‚’ๅ…ฑๆœ‰ใ—ใฆBookloreใฎๆ”นๅ–„ใซใ”ๅ”ๅŠ›ใใ ใ•ใ„ใ€‚ใ“ใฎใƒ‡ใƒผใ‚ฟใซใ‚ˆใ‚Š้–‹็™บ่€…ใฏใ€ๆœ€ใ‚‚ไฝฟ็”จใ•ใ‚Œใฆใ„ใ‚‹ๆฉŸ่ƒฝใฎๆŠŠๆกใ€ใƒใ‚ฐใฎ็‰นๅฎšใ€ใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚นใฎๅ•้กŒใฎ็™บ่ฆ‹ใŒๅฏ่ƒฝใซใชใ‚Šใ€ๆฌกใซๅ–ใ‚Š็ต„ใ‚€ในใใ“ใจใŒใ‚ใ‹ใ‚Šใพใ™ใ€‚ๅ€‹ไบบๆƒ…ๅ ฑใ€ๆœฌใฎๅ†…ๅฎนใ€ใใฎไป–ใฎ่ญ˜ๅˆฅๅฏ่ƒฝใชใƒ‡ใƒผใ‚ฟใฏไธ€ๅˆ‡้€ไฟกใ•ใ‚Œใพใ›ใ‚“ใ€‚ใƒ‡ใƒผใ‚ฟใฏ24ๆ™‚้–“ใ”ใจใซ่‡ชๅ‹•็š„ใซBookloreใ‚ตใƒผใƒใƒผใซ้€ไฟกใ•ใ‚Œใพใ™ใ€‚ๅฎŒๅ…จใซๅฎ‰ๅ…จใงใ€้–‹็™บ่€…ใซใจใฃใฆ้žๅธธใซๆœ‰็”จใงใ™ใ€‚" - }, - "settingsSaved": "่จญๅฎšใ‚’ไฟๅญ˜ใ—ใพใ—ใŸ", - "settingsSavedDetail": "่จญๅฎšใŒๆญฃๅธธใซไฟๅญ˜ใ•ใ‚Œใพใ—ใŸ๏ผ", - "settingsError": "่จญๅฎšใฎไฟๅญ˜ไธญใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸใ€‚", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "ใ‚ฐใƒญใƒผใƒใƒซ่จญๅฎš", + "description": "ใ‚ซใƒใƒผ็”ปๅƒใฎๅ‡ฆ็†ใ€ๆคœ็ดข่จญๅฎšใ€ใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ๅˆถ้™ใชใฉใ€Bookloreใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใฎใ‚ฐใƒญใƒผใƒใƒซ่จญๅฎšใ‚’ๆง‹ๆˆใ—ใพใ™ใ€‚", + "covers": { + "sectionTitle": "ใƒ–ใƒƒใ‚ฏใ‚ซใƒใƒผ็”ปๅƒ", + "regenerate": "ใ‚ซใƒใƒผใ‚’ๅ†็”Ÿๆˆ", + "regenerateBtn": "ๅ†็”Ÿๆˆ", + "regenerateDesc": "ใ™ในใฆใฎๆœฌใฎใ‚ซใƒใƒผ็”ปๅƒใ‚’ใƒ•ใ‚กใ‚คใƒซใซๅŸ‹ใ‚่พผใพใ‚ŒใŸใ‚ซใƒใƒผใ‹ใ‚‰ๅ†็”Ÿๆˆใ—ใพใ™ใ€‚", + "regenerateStarted": "ใ‚ซใƒใƒผๅ†็”Ÿๆˆใ‚’้–‹ๅง‹ใ—ใพใ—ใŸ", + "regenerateStartedDetail": "ใƒ–ใƒƒใ‚ฏใ‚ซใƒใƒผใ‚’ๅ†็”Ÿๆˆไธญใงใ™ใ€‚", + "regenerateError": "ใ‚ซใƒใƒผๅ†็”Ÿๆˆใฎ้–‹ๅง‹ใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚", + "verticalCropping": "็ธฆๆ–นๅ‘ใ‚ซใƒใƒผใƒˆใƒชใƒŸใƒณใ‚ฐ", + "verticalCroppingDesc": "้žๅธธใซ็ธฆ้•ทใฎ็”ปๅƒ๏ผˆWebใ‚ณใƒŸใƒƒใ‚ฏใชใฉ๏ผ‰ใ‚’ไธŠ้ƒจใ‹ใ‚‰่‡ชๅ‹•็š„ใซใƒˆใƒชใƒŸใƒณใ‚ฐใ—ใฆใ€ไฝฟใ„ใ‚„ใ™ใ„ใ‚ซใƒใƒผใ‚ตใƒ ใƒใ‚คใƒซใ‚’ไฝœๆˆใ—ใพใ™ใ€‚", + "horizontalCropping": "ๆจชๆ–นๅ‘ใ‚ซใƒใƒผใƒˆใƒชใƒŸใƒณใ‚ฐ", + "horizontalCroppingDesc": "้žๅธธใซๆจช้•ทใฎ็”ปๅƒใ‚’ๅทฆๅดใ‹ใ‚‰่‡ชๅ‹•็š„ใซใƒˆใƒชใƒŸใƒณใ‚ฐใ—ใฆใ€ไฝฟใ„ใ‚„ใ™ใ„ใ‚ซใƒใƒผใ‚ตใƒ ใƒใ‚คใƒซใ‚’ไฝœๆˆใ—ใพใ™ใ€‚", + "aspectRatio": "ใ‚ขใ‚นใƒšใ‚ฏใƒˆๆฏ”ใ—ใใ„ๅ€ค: {{value}}", + "aspectRatioDesc": "ใ“ใฎใ—ใใ„ๅ€คใ‚’่ถ…ใˆใ‚‹ใ‚ขใ‚นใƒšใ‚ฏใƒˆๆฏ”ใฎ็”ปๅƒใŒใƒˆใƒชใƒŸใƒณใ‚ฐใ•ใ‚Œใพใ™ใ€‚2.5ใฎๅ€คใฏใ€้€šๅธธใฎ2.5ๅ€ไปฅไธŠ็ธฆ้•ท๏ผˆใพใŸใฏๆจช้•ท๏ผ‰ใฎ็”ปๅƒใŒใƒˆใƒชใƒŸใƒณใ‚ฐใ•ใ‚Œใ‚‹ใ“ใจใ‚’ๆ„ๅ‘ณใ—ใพใ™ใ€‚", + "smartCropping": "ใ‚นใƒžใƒผใƒˆใƒˆใƒชใƒŸใƒณใ‚ฐ", + "smartCroppingDesc": "ใƒˆใƒชใƒŸใƒณใ‚ฐไฝ็ฝฎใ‚’ๆฑบๅฎšใ™ใ‚‹้š›ใซๅ‡ไธ€ใช่‰ฒใฎ้ ˜ๅŸŸใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™ใ€‚ใ‚ซใƒใƒผ็”ปๅƒใ‚’ๆœ€ใ‚‚้–ข้€ฃๆ€งใฎ้ซ˜ใ„ใ‚ณใƒณใƒ†ใƒณใƒ„ใซ็„ฆ็‚นใ‚’ๅฝ“ใฆใพใ™ใ€‚" + }, + "search": { + "sectionTitle": "ๆคœ็ดขใจใŠใ™ใ™ใ‚", + "autoBookSearch": "่‡ชๅ‹•ใƒ–ใƒƒใ‚ฏๆคœ็ดข", + "autoBookSearchDesc": "ๆœฌใฎๆƒ…ๅ ฑใƒ‘ใƒใƒซใ‚’้–‹ใ„ใŸใจใใซใ€่‡ชๅ‹•็š„ใซใƒกใ‚ฟใƒ‡ใƒผใ‚ฟใƒžใƒƒใƒใƒณใ‚ฐใ‚’่ฉฆใฟใพใ™ใ€‚", + "similarBook": "้กžไผผๆœฌใฎใŠใ™ใ™ใ‚", + "similarBookDesc": "ใƒฉใ‚คใƒ–ใƒฉใƒชใซๅŸบใฅใ„ใŸ้กžไผผๆœฌใฎใŠใ™ใ™ใ‚ใ‚’ๆœ‰ๅŠนใพใŸใฏ็„กๅŠนใซใ—ใพใ™ใ€‚" + }, + "fileManagement": { + "sectionTitle": "ใƒ•ใ‚กใ‚คใƒซ็ฎก็†", + "maxUploadSize": "ๆœ€ๅคงใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚ตใ‚คใ‚บ", + "maxUploadPlaceholder": "ๆœ€ๅคงใ‚ตใ‚คใ‚บ", + "maxUploadDesc": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใƒ•ใ‚กใ‚คใƒซใฎๆœ€ๅคง่จฑๅฎนใ‚ตใ‚คใ‚บ๏ผˆMBๅ˜ไฝ๏ผ‰ใ‚’ๅฎš็พฉใ—ใพใ™ใ€‚EPUBใ€PDFใ€CBZใ€CBRใ€CB7ๅฝขๅผใซ้ฉ็”จใ•ใ‚Œใพใ™ใ€‚", + "restartWarning": "ๅค‰ๆ›ดใฏใ‚ตใƒผใƒใƒผใฎๅ†่ตทๅ‹•ๅพŒใซๆœ‰ๅŠนใซใชใ‚Šใพใ™", + "invalidInput": "็„กๅŠนใชๅ…ฅๅŠ›", + "invalidInputDetail": "ๆœ‰ๅŠนใชๆœ€ๅคงใƒ•ใ‚กใ‚คใƒซใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚ตใ‚คใ‚บ๏ผˆMB๏ผ‰ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚" + }, + "settingsSaved": "่จญๅฎšใ‚’ไฟๅญ˜ใ—ใพใ—ใŸ", + "settingsSavedDetail": "่จญๅฎšใŒๆญฃๅธธใซไฟๅญ˜ใ•ใ‚Œใพใ—ใŸ๏ผ", + "settingsError": "่จญๅฎšใฎไฟๅญ˜ไธญใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸใ€‚", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/nl/settings-application.json b/booklore-ui/src/i18n/nl/settings-application.json index fff2068f6..0d36dc22a 100644 --- a/booklore-ui/src/i18n/nl/settings-application.json +++ b/booklore-ui/src/i18n/nl/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Globale voorkeuren", - "description": "Configureer globale instellingen voor uw Booklore-instantie, waaronder omslagafbeeldingbeheer, zoekvoorkeuren en uploadlimieten.", - "covers": { - "sectionTitle": "Boekomslagafbeelding", - "regenerate": "Omslagen opnieuw genereren", - "regenerateBtn": "Opnieuw genereren", - "regenerateDesc": "", - "regenerateStarted": "Omslaggeneratie gestart", - "regenerateStartedDetail": "Boekomslagen worden opnieuw gegenereerd.", - "regenerateError": "Kon omslaggeneratie niet starten.", - "verticalCropping": "Verticaal omslagbijsnijden", - "verticalCroppingDesc": "Snijdt automatisch extreem hoge afbeeldingen (zoals webcomics) vanaf de bovenkant bij om bruikbare omslagminiaturen te maken.", - "horizontalCropping": "Horizontaal omslagbijsnijden", - "horizontalCroppingDesc": "Snijdt automatisch extreem brede afbeeldingen vanaf links bij om bruikbare omslagminiaturen te maken.", - "aspectRatio": "Beeldverhoudingsdrempel: {{value}}", - "aspectRatioDesc": "Afbeeldingen met een beeldverhouding boven deze drempel worden bijgesneden. Een waarde van 2,5 betekent dat afbeeldingen meer dan 2,5x hoger (of breder) dan normaal worden bijgesneden.", - "smartCropping": "Slim bijsnijden", - "smartCroppingDesc": "Sla uniforme kleurgebieden over bij het bepalen waar bijgesneden moet worden. Richt de omslagafbeelding op de meest relevante inhoud." - }, - "search": { - "sectionTitle": "Zoeken en aanbevelingen", - "autoBookSearch": "Automatisch boek zoeken", - "autoBookSearchDesc": "Probeert automatisch metadata te matchen wanneer het boekinformatiepaneel wordt geopend.", - "similarBook": "Vergelijkbare boekaanbeveling", - "similarBookDesc": "Schakelt vergelijkbare boekaanbevelingen in of uit op basis van uw bibliotheek." - }, - "fileManagement": { - "sectionTitle": "Bestandsbeheer", - "maxUploadSize": "Max. uploadgrootte", - "maxUploadPlaceholder": "Max. grootte", - "maxUploadDesc": "Bepaalt de maximaal toegestane grootte (in MB) per geรผpload bestand. Van toepassing op alle ondersteunde bestandsformaten.", - "restartWarning": "Wijzigingen worden van kracht na het herstarten van de server", - "invalidInput": "Ongeldige invoer", - "invalidInputDetail": "Voer een geldige maximale uploadgrootte in MB in." - }, - "telemetry": { - "sectionTitle": "Telemetrie", - "enableTelemetry": "Telemetrie inschakelen", - "telemetryDesc": "Help Booklore te verbeteren door anonieme gebruiksstatistieken te delen. Deze gegevens laten de ontwikkelaars zien welke functies het meest worden gebruikt, bugs identificeren en prestatieproblemen opsporen zodat ze weten waar ze aan moeten werken. Er worden nooit persoonlijke gegevens, boekinhoud of identificeerbare gegevens verzonden. Gegevens worden automatisch elke 24 uur naar de Booklore-server verzonden. Het is volledig veilig en zeer nuttig voor de ontwikkelaars." - }, - "settingsSaved": "Instellingen opgeslagen", - "settingsSavedDetail": "De instellingen zijn succesvol opgeslagen!", - "settingsError": "Er is een fout opgetreden bij het opslaan van de instellingen.", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "Globale voorkeuren", + "description": "Configureer globale instellingen voor uw Booklore-instantie, waaronder omslagafbeeldingbeheer, zoekvoorkeuren en uploadlimieten.", + "covers": { + "sectionTitle": "Boekomslagafbeelding", + "regenerate": "Omslagen opnieuw genereren", + "regenerateBtn": "Opnieuw genereren", + "regenerateDesc": "", + "regenerateStarted": "Omslaggeneratie gestart", + "regenerateStartedDetail": "Boekomslagen worden opnieuw gegenereerd.", + "regenerateError": "Kon omslaggeneratie niet starten.", + "verticalCropping": "Verticaal omslagbijsnijden", + "verticalCroppingDesc": "Snijdt automatisch extreem hoge afbeeldingen (zoals webcomics) vanaf de bovenkant bij om bruikbare omslagminiaturen te maken.", + "horizontalCropping": "Horizontaal omslagbijsnijden", + "horizontalCroppingDesc": "Snijdt automatisch extreem brede afbeeldingen vanaf links bij om bruikbare omslagminiaturen te maken.", + "aspectRatio": "Beeldverhoudingsdrempel: {{value}}", + "aspectRatioDesc": "Afbeeldingen met een beeldverhouding boven deze drempel worden bijgesneden. Een waarde van 2,5 betekent dat afbeeldingen meer dan 2,5x hoger (of breder) dan normaal worden bijgesneden.", + "smartCropping": "Slim bijsnijden", + "smartCroppingDesc": "Sla uniforme kleurgebieden over bij het bepalen waar bijgesneden moet worden. Richt de omslagafbeelding op de meest relevante inhoud." + }, + "search": { + "sectionTitle": "Zoeken en aanbevelingen", + "autoBookSearch": "Automatisch boek zoeken", + "autoBookSearchDesc": "Probeert automatisch metadata te matchen wanneer het boekinformatiepaneel wordt geopend.", + "similarBook": "Vergelijkbare boekaanbeveling", + "similarBookDesc": "Schakelt vergelijkbare boekaanbevelingen in of uit op basis van uw bibliotheek." + }, + "fileManagement": { + "sectionTitle": "Bestandsbeheer", + "maxUploadSize": "Max. uploadgrootte", + "maxUploadPlaceholder": "Max. grootte", + "maxUploadDesc": "Bepaalt de maximaal toegestane grootte (in MB) per geรผpload bestand. Van toepassing op alle ondersteunde bestandsformaten.", + "restartWarning": "Wijzigingen worden van kracht na het herstarten van de server", + "invalidInput": "Ongeldige invoer", + "invalidInputDetail": "Voer een geldige maximale uploadgrootte in MB in." + }, + "settingsSaved": "Instellingen opgeslagen", + "settingsSavedDetail": "De instellingen zijn succesvol opgeslagen!", + "settingsError": "Er is een fout opgetreden bij het opslaan van de instellingen.", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/pl/settings-application.json b/booklore-ui/src/i18n/pl/settings-application.json index 6e105c0f8..6d6e8574c 100644 --- a/booklore-ui/src/i18n/pl/settings-application.json +++ b/booklore-ui/src/i18n/pl/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Preferencje globalne", - "description": "Skonfiguruj globalne ustawienia instancji Booklore, w tym obsล‚ugฤ™ obrazรณw okล‚adek, preferencje wyszukiwania i limity przesyล‚ania plikรณw.", - "covers": { - "sectionTitle": "Obraz okล‚adki ksiฤ…ลผki", - "regenerate": "Ponownie wygeneruj okล‚adki", - "regenerateBtn": "Ponownie wygeneruj", - "regenerateDesc": "Ponownie generuje obrazy okล‚adek dla wszystkich ksiฤ…ลผek z osadzonych okล‚adek w plikach.", - "regenerateStarted": "Rozpoczฤ™to ponowne generowanie okล‚adek", - "regenerateStartedDetail": "Okล‚adki ksiฤ…ลผek sฤ… ponownie generowane.", - "regenerateError": "Nie udaล‚o siฤ™ rozpoczฤ…ฤ‡ ponownego generowania okล‚adek.", - "verticalCropping": "Pionowe przycinanie okล‚adek", - "verticalCroppingDesc": "Automatycznie przycinaj wyjฤ…tkowo wysokie obrazy (jak komiksy webowe) od gรณry, aby utworzyฤ‡ uลผyteczne miniaturki okล‚adek.", - "horizontalCropping": "Poziome przycinanie okล‚adek", - "horizontalCroppingDesc": "Automatycznie przycinaj wyjฤ…tkowo szerokie obrazy od lewej, aby utworzyฤ‡ uลผyteczne miniaturki okล‚adek.", - "aspectRatio": "Prรณg proporcji: {{value}}", - "aspectRatioDesc": "Obrazy o proporcjach przekraczajฤ…cych ten prรณg zostanฤ… przyciฤ™te. Wartoล›ฤ‡ 2,5 oznacza, ลผe obrazy ponad 2,5x wyลผsze (lub szersze) niลผ normalne zostanฤ… przyciฤ™te.", - "smartCropping": "Inteligentne przycinanie", - "smartCroppingDesc": "Pomijaj jednokolorowe regiony przy okreล›laniu miejsca przycinania. Skupia obraz okล‚adki na najbardziej istotnej treล›ci." - }, - "search": { - "sectionTitle": "Wyszukiwanie i rekomendacje", - "autoBookSearch": "Automatyczne wyszukiwanie ksiฤ…ลผek", - "autoBookSearchDesc": "Automatycznie prรณbuje dopasowaฤ‡ metadane po otwarciu panelu informacji o ksiฤ…ลผce.", - "similarBook": "Rekomendacje podobnych ksiฤ…ลผek", - "similarBookDesc": "Wล‚ฤ…cza lub wyล‚ฤ…cza rekomendacje podobnych ksiฤ…ลผek na podstawie Twojej biblioteki." - }, - "fileManagement": { - "sectionTitle": "Zarzฤ…dzanie plikami", - "maxUploadSize": "Maks. rozmiar przesyล‚anego pliku", - "maxUploadPlaceholder": "Maks. rozmiar", - "maxUploadDesc": "Okreล›la maksymalny dozwolony rozmiar (w MB) kaลผdego przesyล‚anego pliku. Dotyczy formatรณw EPUB, PDF, CBZ, CBR i CB7.", - "restartWarning": "Zmiany zostanฤ… zastosowane po ponownym uruchomieniu serwera", - "invalidInput": "Nieprawidล‚owe dane", - "invalidInputDetail": "Proszฤ™ wprowadziฤ‡ prawidล‚owy maksymalny rozmiar przesyล‚anego pliku w MB." - }, - "telemetry": { - "sectionTitle": "Telemetria", - "enableTelemetry": "Wล‚ฤ…cz telemetriฤ™", - "telemetryDesc": "Pomรณลผ ulepszyฤ‡ Booklore, udostฤ™pniajฤ…c anonimowe statystyki uลผytkowania. Te dane pozwalajฤ… deweloperom zobaczyฤ‡, ktรณre funkcje sฤ… najczฤ™ล›ciej uลผywane, identyfikowaฤ‡ bล‚ฤ™dy i wykrywaฤ‡ problemy z wydajnoล›ciฤ…, aby wiedzieli, nad czym pracowaฤ‡ dalej. ลปadne dane osobowe, treล›ci ksiฤ…ลผek ani ลผadne identyfikowalne dane nigdy nie sฤ… wysyล‚ane. Dane sฤ… automatycznie wysyล‚ane na serwer Booklore raz na 24 godziny. Jest to caล‚kowicie bezpieczne i bardzo pomocne dla deweloperรณw." - }, - "settingsSaved": "Ustawienia zapisane", - "settingsSavedDetail": "Ustawienia zostaล‚y pomyล›lnie zapisane!", - "settingsError": "Wystฤ…piล‚ bล‚ฤ…d podczas zapisywania ustawieล„.", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "Preferencje globalne", + "description": "Skonfiguruj globalne ustawienia instancji Booklore, w tym obsล‚ugฤ™ obrazรณw okล‚adek, preferencje wyszukiwania i limity przesyล‚ania plikรณw.", + "covers": { + "sectionTitle": "Obraz okล‚adki ksiฤ…ลผki", + "regenerate": "Ponownie wygeneruj okล‚adki", + "regenerateBtn": "Ponownie wygeneruj", + "regenerateDesc": "Ponownie generuje obrazy okล‚adek dla wszystkich ksiฤ…ลผek z osadzonych okล‚adek w plikach.", + "regenerateStarted": "Rozpoczฤ™to ponowne generowanie okล‚adek", + "regenerateStartedDetail": "Okล‚adki ksiฤ…ลผek sฤ… ponownie generowane.", + "regenerateError": "Nie udaล‚o siฤ™ rozpoczฤ…ฤ‡ ponownego generowania okล‚adek.", + "verticalCropping": "Pionowe przycinanie okล‚adek", + "verticalCroppingDesc": "Automatycznie przycinaj wyjฤ…tkowo wysokie obrazy (jak komiksy webowe) od gรณry, aby utworzyฤ‡ uลผyteczne miniaturki okล‚adek.", + "horizontalCropping": "Poziome przycinanie okล‚adek", + "horizontalCroppingDesc": "Automatycznie przycinaj wyjฤ…tkowo szerokie obrazy od lewej, aby utworzyฤ‡ uลผyteczne miniaturki okล‚adek.", + "aspectRatio": "Prรณg proporcji: {{value}}", + "aspectRatioDesc": "Obrazy o proporcjach przekraczajฤ…cych ten prรณg zostanฤ… przyciฤ™te. Wartoล›ฤ‡ 2,5 oznacza, ลผe obrazy ponad 2,5x wyลผsze (lub szersze) niลผ normalne zostanฤ… przyciฤ™te.", + "smartCropping": "Inteligentne przycinanie", + "smartCroppingDesc": "Pomijaj jednokolorowe regiony przy okreล›laniu miejsca przycinania. Skupia obraz okล‚adki na najbardziej istotnej treล›ci." + }, + "search": { + "sectionTitle": "Wyszukiwanie i rekomendacje", + "autoBookSearch": "Automatyczne wyszukiwanie ksiฤ…ลผek", + "autoBookSearchDesc": "Automatycznie prรณbuje dopasowaฤ‡ metadane po otwarciu panelu informacji o ksiฤ…ลผce.", + "similarBook": "Rekomendacje podobnych ksiฤ…ลผek", + "similarBookDesc": "Wล‚ฤ…cza lub wyล‚ฤ…cza rekomendacje podobnych ksiฤ…ลผek na podstawie Twojej biblioteki." + }, + "fileManagement": { + "sectionTitle": "Zarzฤ…dzanie plikami", + "maxUploadSize": "Maks. rozmiar przesyล‚anego pliku", + "maxUploadPlaceholder": "Maks. rozmiar", + "maxUploadDesc": "Okreล›la maksymalny dozwolony rozmiar (w MB) kaลผdego przesyล‚anego pliku. Dotyczy formatรณw EPUB, PDF, CBZ, CBR i CB7.", + "restartWarning": "Zmiany zostanฤ… zastosowane po ponownym uruchomieniu serwera", + "invalidInput": "Nieprawidล‚owe dane", + "invalidInputDetail": "Proszฤ™ wprowadziฤ‡ prawidล‚owy maksymalny rozmiar przesyล‚anego pliku w MB." + }, + "settingsSaved": "Ustawienia zapisane", + "settingsSavedDetail": "Ustawienia zostaล‚y pomyล›lnie zapisane!", + "settingsError": "Wystฤ…piล‚ bล‚ฤ…d podczas zapisywania ustawieล„.", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/pt/settings-application.json b/booklore-ui/src/i18n/pt/settings-application.json index 6f1ff0247..708d67bc6 100644 --- a/booklore-ui/src/i18n/pt/settings-application.json +++ b/booklore-ui/src/i18n/pt/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Preferรชncias Globais", - "description": "Configure definiรงรตes globais para sua instรขncia do Booklore, incluindo tratamento de imagens de capa, preferรชncias de pesquisa e limites de envio de arquivos.", - "covers": { - "sectionTitle": "Imagem de Capa do Livro", - "regenerate": "Regenerar Capas", - "regenerateBtn": "Regenerar", - "regenerateDesc": "Regenera as imagens de capa de todos os livros a partir das capas incorporadas nos arquivos.", - "regenerateStarted": "Regeneraรงรฃo de Capas Iniciada", - "regenerateStartedDetail": "As capas dos livros estรฃo sendo regeneradas.", - "regenerateError": "Falha ao iniciar a regeneraรงรฃo de capas.", - "verticalCropping": "Corte Vertical de Capa", - "verticalCroppingDesc": "Cortar automaticamente imagens extremamente altas (como web comics) a partir do topo para criar miniaturas de capa utilizรกveis.", - "horizontalCropping": "Corte Horizontal de Capa", - "horizontalCroppingDesc": "Cortar automaticamente imagens extremamente largas a partir da esquerda para criar miniaturas de capa utilizรกveis.", - "aspectRatio": "Limiar de Proporรงรฃo: {{value}}", - "aspectRatioDesc": "Imagens com proporรงรตes que excedem este limiar serรฃo cortadas. Um valor de 2.5 significa que imagens mais de 2.5x mais altas (ou mais largas) que o normal serรฃo cortadas.", - "smartCropping": "Corte Inteligente", - "smartCroppingDesc": "Ignorar regiรตes de cor uniforme ao determinar onde cortar. Foca a imagem da capa no conteรบdo mais relevante." - }, - "search": { - "sectionTitle": "Pesquisa e Recomendaรงรตes", - "autoBookSearch": "Pesquisa Automรกtica de Livro", - "autoBookSearchDesc": "Tenta automaticamente a correspondรชncia de metadados quando o painel de informaรงรตes do livro รฉ aberto.", - "similarBook": "Recomendaรงรฃo de Livros Similares", - "similarBookDesc": "Ativa ou desativa recomendaรงรตes de livros similares baseadas na sua biblioteca." - }, - "fileManagement": { - "sectionTitle": "Gerenciamento de Arquivos", - "maxUploadSize": "Tamanho Mรกximo de Upload de Arquivo", - "maxUploadPlaceholder": "Tamanho mรกximo", - "maxUploadDesc": "Define o tamanho mรกximo permitido (em MB) para cada arquivo enviado. Aplica-se aos formatos EPUB, PDF, CBZ, CBR e CB7.", - "restartWarning": "As alteraรงรตes terรฃo efeito apรณs reiniciar o servidor", - "invalidInput": "Entrada Invรกlida", - "invalidInputDetail": "Digite um tamanho mรกximo de upload de arquivo vรกlido em MB." - }, - "telemetry": { - "sectionTitle": "Telemetria", - "enableTelemetry": "Ativar Telemetria", - "telemetryDesc": "Ajude a melhorar o Booklore compartilhando estatรญsticas de uso anรดnimas. Esses dados permitem que os desenvolvedores vejam quais recursos sรฃo mais usados, identifiquem bugs e detectem problemas de desempenho para saberem no que trabalhar em seguida. Nenhuma informaรงรฃo pessoal, conteรบdo de livros ou dados identificรกveis sรฃo enviados. Os dados sรฃo enviados automaticamente ao servidor do Booklore uma vez a cada 24 horas. ร‰ completamente seguro e muito รบtil para os desenvolvedores." - }, - "settingsSaved": "Configuraรงรตes Salvas", - "settingsSavedDetail": "As configuraรงรตes foram salvas com sucesso!", - "settingsError": "Houve um erro ao salvar as configuraรงรตes.", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "Preferรชncias Globais", + "description": "Configure definiรงรตes globais para sua instรขncia do Booklore, incluindo tratamento de imagens de capa, preferรชncias de pesquisa e limites de envio de arquivos.", + "covers": { + "sectionTitle": "Imagem de Capa do Livro", + "regenerate": "Regenerar Capas", + "regenerateBtn": "Regenerar", + "regenerateDesc": "Regenera as imagens de capa de todos os livros a partir das capas incorporadas nos arquivos.", + "regenerateStarted": "Regeneraรงรฃo de Capas Iniciada", + "regenerateStartedDetail": "As capas dos livros estรฃo sendo regeneradas.", + "regenerateError": "Falha ao iniciar a regeneraรงรฃo de capas.", + "verticalCropping": "Corte Vertical de Capa", + "verticalCroppingDesc": "Cortar automaticamente imagens extremamente altas (como web comics) a partir do topo para criar miniaturas de capa utilizรกveis.", + "horizontalCropping": "Corte Horizontal de Capa", + "horizontalCroppingDesc": "Cortar automaticamente imagens extremamente largas a partir da esquerda para criar miniaturas de capa utilizรกveis.", + "aspectRatio": "Limiar de Proporรงรฃo: {{value}}", + "aspectRatioDesc": "Imagens com proporรงรตes que excedem este limiar serรฃo cortadas. Um valor de 2.5 significa que imagens mais de 2.5x mais altas (ou mais largas) que o normal serรฃo cortadas.", + "smartCropping": "Corte Inteligente", + "smartCroppingDesc": "Ignorar regiรตes de cor uniforme ao determinar onde cortar. Foca a imagem da capa no conteรบdo mais relevante." + }, + "search": { + "sectionTitle": "Pesquisa e Recomendaรงรตes", + "autoBookSearch": "Pesquisa Automรกtica de Livro", + "autoBookSearchDesc": "Tenta automaticamente a correspondรชncia de metadados quando o painel de informaรงรตes do livro รฉ aberto.", + "similarBook": "Recomendaรงรฃo de Livros Similares", + "similarBookDesc": "Ativa ou desativa recomendaรงรตes de livros similares baseadas na sua biblioteca." + }, + "fileManagement": { + "sectionTitle": "Gerenciamento de Arquivos", + "maxUploadSize": "Tamanho Mรกximo de Upload de Arquivo", + "maxUploadPlaceholder": "Tamanho mรกximo", + "maxUploadDesc": "Define o tamanho mรกximo permitido (em MB) para cada arquivo enviado. Aplica-se aos formatos EPUB, PDF, CBZ, CBR e CB7.", + "restartWarning": "As alteraรงรตes terรฃo efeito apรณs reiniciar o servidor", + "invalidInput": "Entrada Invรกlida", + "invalidInputDetail": "Digite um tamanho mรกximo de upload de arquivo vรกlido em MB." + }, + "settingsSaved": "Configuraรงรตes Salvas", + "settingsSavedDetail": "As configuraรงรตes foram salvas com sucesso!", + "settingsError": "Houve um erro ao salvar as configuraรงรตes.", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/ru/settings-application.json b/booklore-ui/src/i18n/ru/settings-application.json index 5b13db8ae..a431d2e2e 100644 --- a/booklore-ui/src/i18n/ru/settings-application.json +++ b/booklore-ui/src/i18n/ru/settings-application.json @@ -1,50 +1,45 @@ { - "title": "ะ“ะปะพะฑะฐะปัŒะฝั‹ะต ะฝะฐัั‚ั€ะพะนะบะธ", - "description": "ะะฐัั‚ั€ะพะนั‚ะต ะณะปะพะฑะฐะปัŒะฝั‹ะต ะฟะฐั€ะฐะผะตั‚ั€ั‹ ะ’ะฐัˆะตะณะพ ัะบะทะตะผะฟะปัั€ะฐ Booklore, ะฒะบะปัŽั‡ะฐั ะพะฑั€ะฐะฑะพั‚ะบัƒ ะพะฑะปะพะถะตะบ, ะฟะฐั€ะฐะผะตั‚ั€ั‹ ะฟะพะธัะบะฐ ะธ ะพะณั€ะฐะฝะธั‡ะตะฝะธั ะทะฐะณั€ัƒะทะบะธ ั„ะฐะนะปะพะฒ.", - "covers": { - "sectionTitle": "ะžะฑะปะพะถะบะธ ะบะฝะธะณ", - "regenerate": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ะธั ะพะฑะปะพะถะตะบ", - "regenerateBtn": "ะŸะตั€ะตะณะตะฝะตั€ะธั€ะพะฒะฐั‚ัŒ", - "regenerateDesc": "ะŸะตั€ะตะณะตะฝะตั€ะธั€ัƒะตั‚ ะพะฑะปะพะถะบะธ ะดะปั ะฒัะตั… ะบะฝะธะณ ะธะท ะฒัั‚ั€ะพะตะฝะฝั‹ั… ะพะฑะปะพะถะตะบ ะฒ ั„ะฐะนะปะฐั….", - "regenerateStarted": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ะธั ะพะฑะปะพะถะตะบ ะทะฐะฟัƒั‰ะตะฝะฐ", - "regenerateStartedDetail": "ะžะฑะปะพะถะบะธ ะบะฝะธะณ ะฟะตั€ะตะณะตะฝะตั€ะธั€ัƒัŽั‚ัั.", - "regenerateError": "ะะต ัƒะดะฐะปะพััŒ ะทะฐะฟัƒัั‚ะธั‚ัŒ ะฟะตั€ะตะณะตะฝะตั€ะฐั†ะธัŽ ะพะฑะปะพะถะตะบ.", - "verticalCropping": "ะ’ะตั€ั‚ะธะบะฐะปัŒะฝะฐั ะพะฑั€ะตะทะบะฐ ะพะฑะปะพะถะตะบ", - "verticalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะพะฑั€ะตะทะฐั‚ัŒ ั‡ั€ะตะทะผะตั€ะฝะพ ะฒั‹ัะพะบะธะต ะธะทะพะฑั€ะฐะถะตะฝะธั (ะบะฐะบ ะฒะตะฑ-ะบะพะผะธะบัั‹) ัะฒะตั€ั…ัƒ ะดะปั ัะพะทะดะฐะฝะธั ะฟั€ะธะณะพะดะฝั‹ั… ะผะธะฝะธะฐั‚ัŽั€ ะพะฑะปะพะถะตะบ.", - "horizontalCropping": "ะ“ะพั€ะธะทะพะฝั‚ะฐะปัŒะฝะฐั ะพะฑั€ะตะทะบะฐ ะพะฑะปะพะถะตะบ", - "horizontalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะพะฑั€ะตะทะฐั‚ัŒ ั‡ั€ะตะทะผะตั€ะฝะพ ัˆะธั€ะพะบะธะต ะธะทะพะฑั€ะฐะถะตะฝะธั ัะปะตะฒะฐ ะดะปั ัะพะทะดะฐะฝะธั ะฟั€ะธะณะพะดะฝั‹ั… ะผะธะฝะธะฐั‚ัŽั€ ะพะฑะปะพะถะตะบ.", - "aspectRatio": "ะŸะพั€ะพะณ ัะพะพั‚ะฝะพัˆะตะฝะธั ัั‚ะพั€ะพะฝ: {{value}}", - "aspectRatioDesc": "ะ˜ะทะพะฑั€ะฐะถะตะฝะธั, ัะพะพั‚ะฝะพัˆะตะฝะธะต ัั‚ะพั€ะพะฝ ะบะพั‚ะพั€ั‹ั… ะฟั€ะตะฒั‹ัˆะฐะตั‚ ัั‚ะพั‚ ะฟะพั€ะพะณ, ะฑัƒะดัƒั‚ ะพะฑั€ะตะทะฐะฝั‹. ะ—ะฝะฐั‡ะตะฝะธะต 2.5 ะพะทะฝะฐั‡ะฐะตั‚, ั‡ั‚ะพ ะธะทะพะฑั€ะฐะถะตะฝะธั ะฑะพะปะตะต ั‡ะตะผ ะฒ 2.5 ั€ะฐะทะฐ ะฒั‹ัˆะต (ะธะปะธ ัˆะธั€ะต) ะฝะพั€ะผั‹ ะฑัƒะดัƒั‚ ะพะฑั€ะตะทะฐะฝั‹.", - "smartCropping": "ะฃะผะฝะฐั ะพะฑั€ะตะทะบะฐ", - "smartCroppingDesc": "ะŸั€ะพะฟัƒัะบะฐั‚ัŒ ะพะดะฝะพั‚ะพะฝะฝั‹ะต ะพะฑะปะฐัั‚ะธ ะฟั€ะธ ะพะฟั€ะตะดะตะปะตะฝะธะธ ะผะตัั‚ะฐ ะพะฑั€ะตะทะบะธ. ะคะพะบัƒัะธั€ัƒะตั‚ ะพะฑะปะพะถะบัƒ ะฝะฐ ะฝะฐะธะฑะพะปะตะต ะทะฝะฐั‡ะธะผะพะผ ัะพะดะตั€ะถะฐะฝะธะธ." - }, - "search": { - "sectionTitle": "ะŸะพะธัะบ ะธ ั€ะตะบะพะผะตะฝะดะฐั†ะธะธ", - "autoBookSearch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธะน ะฟะพะธัะบ ะบะฝะธะณ", - "autoBookSearchDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะฟั‹ั‚ะฐะตั‚ัั ัะพะฟะพัั‚ะฐะฒะธั‚ัŒ ะผะตั‚ะฐะดะฐะฝะฝั‹ะต ะฟั€ะธ ะพั‚ะบั€ั‹ั‚ะธะธ ะฟะฐะฝะตะปะธ ะธะฝั„ะพั€ะผะฐั†ะธะธ ะพ ะบะฝะธะณะต.", - "similarBook": "ะ ะตะบะพะผะตะฝะดะฐั†ะธะธ ะฟะพั…ะพะถะธั… ะบะฝะธะณ", - "similarBookDesc": "ะ’ะบะปัŽั‡ะฐะตั‚ ะธะปะธ ะพั‚ะบะปัŽั‡ะฐะตั‚ ั€ะตะบะพะผะตะฝะดะฐั†ะธะธ ะฟะพั…ะพะถะธั… ะบะฝะธะณ ะฝะฐ ะพัะฝะพะฒะต ะ’ะฐัˆะตะน ะฑะธะฑะปะธะพั‚ะตะบะธ." - }, - "fileManagement": { - "sectionTitle": "ะฃะฟั€ะฐะฒะปะตะฝะธะต ั„ะฐะนะปะฐะผะธ", - "maxUploadSize": "ะœะฐะบัะธะผะฐะปัŒะฝั‹ะน ั€ะฐะทะผะตั€ ะทะฐะณั€ัƒะถะฐะตะผะพะณะพ ั„ะฐะนะปะฐ", - "maxUploadPlaceholder": "ะœะฐะบั. ั€ะฐะทะผะตั€", - "maxUploadDesc": "ะžะฟั€ะตะดะตะปัะตั‚ ะผะฐะบัะธะผะฐะปัŒะฝะพ ะดะพะฟัƒัั‚ะธะผั‹ะน ั€ะฐะทะผะตั€ (ะฒ ะœะ‘) ะดะปั ะบะฐะถะดะพะณะพ ะทะฐะณั€ัƒะถะฐะตะผะพะณะพ ั„ะฐะนะปะฐ. ะŸั€ะธะผะตะฝัะตั‚ัั ะบ ั„ะพั€ะผะฐั‚ะฐะผ EPUB, PDF, CBZ, CBR ะธ CB7.", - "restartWarning": "ะ˜ะทะผะตะฝะตะฝะธั ะฒัั‚ัƒะฟัั‚ ะฒ ัะธะปัƒ ะฟะพัะปะต ะฟะตั€ะตะทะฐะฟัƒัะบะฐ ัะตั€ะฒะตั€ะฐ", - "invalidInput": "ะะตะฒะตั€ะฝั‹ะต ะดะฐะฝะฝั‹ะต", - "invalidInputDetail": "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะฒะตะดะธั‚ะต ะบะพั€ั€ะตะบั‚ะฝั‹ะน ะผะฐะบัะธะผะฐะปัŒะฝั‹ะน ั€ะฐะทะผะตั€ ั„ะฐะนะปะฐ ะฒ ะœะ‘." - }, - "telemetry": { - "sectionTitle": "ะขะตะปะตะผะตั‚ั€ะธั", - "enableTelemetry": "ะ’ะบะปัŽั‡ะธั‚ัŒ ั‚ะตะปะตะผะตั‚ั€ะธัŽ", - "telemetryDesc": "ะŸะพะผะพะณะธั‚ะต ัƒะปัƒั‡ัˆะธั‚ัŒ Booklore, ะดะตะปัััŒ ะฐะฝะพะฝะธะผะฝะพะน ัั‚ะฐั‚ะธัั‚ะธะบะพะน ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั. ะญั‚ะธ ะดะฐะฝะฝั‹ะต ะฟะพะทะฒะพะปััŽั‚ ั€ะฐะทั€ะฐะฑะพั‚ั‡ะธะบะฐะผ ะฒะธะดะตั‚ัŒ, ะบะฐะบะธะต ั„ัƒะฝะบั†ะธะธ ะฝะฐะธะฑะพะปะตะต ะฒะพัั‚ั€ะตะฑะพะฒะฐะฝั‹, ะฒั‹ัะฒะปัั‚ัŒ ะพัˆะธะฑะบะธ ะธ ะพะฑะฝะฐั€ัƒะถะธะฒะฐั‚ัŒ ะฟั€ะพะฑะปะตะผั‹ ั ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝะพัั‚ัŒัŽ, ั‡ั‚ะพะฑั‹ ะทะฝะฐั‚ัŒ, ะฝะฐะด ั‡ะตะผ ั€ะฐะฑะพั‚ะฐั‚ัŒ ะดะฐะปัŒัˆะต. ะ›ะธั‡ะฝะฐั ะธะฝั„ะพั€ะผะฐั†ะธั, ัะพะดะตั€ะถะฐะฝะธะต ะบะฝะธะณ ะธ ะปัŽะฑั‹ะต ะธะดะตะฝั‚ะธั„ะธั†ะธั€ัƒัŽั‰ะธะต ะดะฐะฝะฝั‹ะต ะฝะธะบะพะณะดะฐ ะฝะต ะฟะตั€ะตะดะฐัŽั‚ัั. ะ”ะฐะฝะฝั‹ะต ะพั‚ะฟั€ะฐะฒะปััŽั‚ัั ะฝะฐ ัะตั€ะฒะตั€ Booklore ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ั€ะฐะท ะฒ 24 ั‡ะฐัะฐ. ะญั‚ะพ ะฟะพะปะฝะพัั‚ัŒัŽ ะฑะตะทะพะฟะฐัะฝะพ ะธ ะพั‡ะตะฝัŒ ะฟะพะปะตะทะฝะพ ะดะปั ั€ะฐะทั€ะฐะฑะพั‚ั‡ะธะบะพะฒ." - }, - "settingsSaved": "ะะฐัั‚ั€ะพะนะบะธ ัะพั…ั€ะฐะฝะตะฝั‹", - "settingsSavedDetail": "ะะฐัั‚ั€ะพะนะบะธ ะฑั‹ะปะธ ัƒัะฟะตัˆะฝะพ ัะพั…ั€ะฐะฝะตะฝั‹!", - "settingsError": "ะŸั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ ะฟั€ะธ ัะพั…ั€ะฐะฝะตะฝะธะธ ะฝะฐัั‚ั€ะพะตะบ.", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "ะ“ะปะพะฑะฐะปัŒะฝั‹ะต ะฝะฐัั‚ั€ะพะนะบะธ", + "description": "ะะฐัั‚ั€ะพะนั‚ะต ะณะปะพะฑะฐะปัŒะฝั‹ะต ะฟะฐั€ะฐะผะตั‚ั€ั‹ ะ’ะฐัˆะตะณะพ ัะบะทะตะผะฟะปัั€ะฐ Booklore, ะฒะบะปัŽั‡ะฐั ะพะฑั€ะฐะฑะพั‚ะบัƒ ะพะฑะปะพะถะตะบ, ะฟะฐั€ะฐะผะตั‚ั€ั‹ ะฟะพะธัะบะฐ ะธ ะพะณั€ะฐะฝะธั‡ะตะฝะธั ะทะฐะณั€ัƒะทะบะธ ั„ะฐะนะปะพะฒ.", + "covers": { + "sectionTitle": "ะžะฑะปะพะถะบะธ ะบะฝะธะณ", + "regenerate": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ะธั ะพะฑะปะพะถะตะบ", + "regenerateBtn": "ะŸะตั€ะตะณะตะฝะตั€ะธั€ะพะฒะฐั‚ัŒ", + "regenerateDesc": "ะŸะตั€ะตะณะตะฝะตั€ะธั€ัƒะตั‚ ะพะฑะปะพะถะบะธ ะดะปั ะฒัะตั… ะบะฝะธะณ ะธะท ะฒัั‚ั€ะพะตะฝะฝั‹ั… ะพะฑะปะพะถะตะบ ะฒ ั„ะฐะนะปะฐั….", + "regenerateStarted": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ะธั ะพะฑะปะพะถะตะบ ะทะฐะฟัƒั‰ะตะฝะฐ", + "regenerateStartedDetail": "ะžะฑะปะพะถะบะธ ะบะฝะธะณ ะฟะตั€ะตะณะตะฝะตั€ะธั€ัƒัŽั‚ัั.", + "regenerateError": "ะะต ัƒะดะฐะปะพััŒ ะทะฐะฟัƒัั‚ะธั‚ัŒ ะฟะตั€ะตะณะตะฝะตั€ะฐั†ะธัŽ ะพะฑะปะพะถะตะบ.", + "verticalCropping": "ะ’ะตั€ั‚ะธะบะฐะปัŒะฝะฐั ะพะฑั€ะตะทะบะฐ ะพะฑะปะพะถะตะบ", + "verticalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะพะฑั€ะตะทะฐั‚ัŒ ั‡ั€ะตะทะผะตั€ะฝะพ ะฒั‹ัะพะบะธะต ะธะทะพะฑั€ะฐะถะตะฝะธั (ะบะฐะบ ะฒะตะฑ-ะบะพะผะธะบัั‹) ัะฒะตั€ั…ัƒ ะดะปั ัะพะทะดะฐะฝะธั ะฟั€ะธะณะพะดะฝั‹ั… ะผะธะฝะธะฐั‚ัŽั€ ะพะฑะปะพะถะตะบ.", + "horizontalCropping": "ะ“ะพั€ะธะทะพะฝั‚ะฐะปัŒะฝะฐั ะพะฑั€ะตะทะบะฐ ะพะฑะปะพะถะตะบ", + "horizontalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะพะฑั€ะตะทะฐั‚ัŒ ั‡ั€ะตะทะผะตั€ะฝะพ ัˆะธั€ะพะบะธะต ะธะทะพะฑั€ะฐะถะตะฝะธั ัะปะตะฒะฐ ะดะปั ัะพะทะดะฐะฝะธั ะฟั€ะธะณะพะดะฝั‹ั… ะผะธะฝะธะฐั‚ัŽั€ ะพะฑะปะพะถะตะบ.", + "aspectRatio": "ะŸะพั€ะพะณ ัะพะพั‚ะฝะพัˆะตะฝะธั ัั‚ะพั€ะพะฝ: {{value}}", + "aspectRatioDesc": "ะ˜ะทะพะฑั€ะฐะถะตะฝะธั, ัะพะพั‚ะฝะพัˆะตะฝะธะต ัั‚ะพั€ะพะฝ ะบะพั‚ะพั€ั‹ั… ะฟั€ะตะฒั‹ัˆะฐะตั‚ ัั‚ะพั‚ ะฟะพั€ะพะณ, ะฑัƒะดัƒั‚ ะพะฑั€ะตะทะฐะฝั‹. ะ—ะฝะฐั‡ะตะฝะธะต 2.5 ะพะทะฝะฐั‡ะฐะตั‚, ั‡ั‚ะพ ะธะทะพะฑั€ะฐะถะตะฝะธั ะฑะพะปะตะต ั‡ะตะผ ะฒ 2.5 ั€ะฐะทะฐ ะฒั‹ัˆะต (ะธะปะธ ัˆะธั€ะต) ะฝะพั€ะผั‹ ะฑัƒะดัƒั‚ ะพะฑั€ะตะทะฐะฝั‹.", + "smartCropping": "ะฃะผะฝะฐั ะพะฑั€ะตะทะบะฐ", + "smartCroppingDesc": "ะŸั€ะพะฟัƒัะบะฐั‚ัŒ ะพะดะฝะพั‚ะพะฝะฝั‹ะต ะพะฑะปะฐัั‚ะธ ะฟั€ะธ ะพะฟั€ะตะดะตะปะตะฝะธะธ ะผะตัั‚ะฐ ะพะฑั€ะตะทะบะธ. ะคะพะบัƒัะธั€ัƒะตั‚ ะพะฑะปะพะถะบัƒ ะฝะฐ ะฝะฐะธะฑะพะปะตะต ะทะฝะฐั‡ะธะผะพะผ ัะพะดะตั€ะถะฐะฝะธะธ." + }, + "search": { + "sectionTitle": "ะŸะพะธัะบ ะธ ั€ะตะบะพะผะตะฝะดะฐั†ะธะธ", + "autoBookSearch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธะน ะฟะพะธัะบ ะบะฝะธะณ", + "autoBookSearchDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ ะฟั‹ั‚ะฐะตั‚ัั ัะพะฟะพัั‚ะฐะฒะธั‚ัŒ ะผะตั‚ะฐะดะฐะฝะฝั‹ะต ะฟั€ะธ ะพั‚ะบั€ั‹ั‚ะธะธ ะฟะฐะฝะตะปะธ ะธะฝั„ะพั€ะผะฐั†ะธะธ ะพ ะบะฝะธะณะต.", + "similarBook": "ะ ะตะบะพะผะตะฝะดะฐั†ะธะธ ะฟะพั…ะพะถะธั… ะบะฝะธะณ", + "similarBookDesc": "ะ’ะบะปัŽั‡ะฐะตั‚ ะธะปะธ ะพั‚ะบะปัŽั‡ะฐะตั‚ ั€ะตะบะพะผะตะฝะดะฐั†ะธะธ ะฟะพั…ะพะถะธั… ะบะฝะธะณ ะฝะฐ ะพัะฝะพะฒะต ะ’ะฐัˆะตะน ะฑะธะฑะปะธะพั‚ะตะบะธ." + }, + "fileManagement": { + "sectionTitle": "ะฃะฟั€ะฐะฒะปะตะฝะธะต ั„ะฐะนะปะฐะผะธ", + "maxUploadSize": "ะœะฐะบัะธะผะฐะปัŒะฝั‹ะน ั€ะฐะทะผะตั€ ะทะฐะณั€ัƒะถะฐะตะผะพะณะพ ั„ะฐะนะปะฐ", + "maxUploadPlaceholder": "ะœะฐะบั. ั€ะฐะทะผะตั€", + "maxUploadDesc": "ะžะฟั€ะตะดะตะปัะตั‚ ะผะฐะบัะธะผะฐะปัŒะฝะพ ะดะพะฟัƒัั‚ะธะผั‹ะน ั€ะฐะทะผะตั€ (ะฒ ะœะ‘) ะดะปั ะบะฐะถะดะพะณะพ ะทะฐะณั€ัƒะถะฐะตะผะพะณะพ ั„ะฐะนะปะฐ. ะŸั€ะธะผะตะฝัะตั‚ัั ะบ ั„ะพั€ะผะฐั‚ะฐะผ EPUB, PDF, CBZ, CBR ะธ CB7.", + "restartWarning": "ะ˜ะทะผะตะฝะตะฝะธั ะฒัั‚ัƒะฟัั‚ ะฒ ัะธะปัƒ ะฟะพัะปะต ะฟะตั€ะตะทะฐะฟัƒัะบะฐ ัะตั€ะฒะตั€ะฐ", + "invalidInput": "ะะตะฒะตั€ะฝั‹ะต ะดะฐะฝะฝั‹ะต", + "invalidInputDetail": "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะฒะตะดะธั‚ะต ะบะพั€ั€ะตะบั‚ะฝั‹ะน ะผะฐะบัะธะผะฐะปัŒะฝั‹ะน ั€ะฐะทะผะตั€ ั„ะฐะนะปะฐ ะฒ ะœะ‘." + }, + "settingsSaved": "ะะฐัั‚ั€ะพะนะบะธ ัะพั…ั€ะฐะฝะตะฝั‹", + "settingsSavedDetail": "ะะฐัั‚ั€ะพะนะบะธ ะฑั‹ะปะธ ัƒัะฟะตัˆะฝะพ ัะพั…ั€ะฐะฝะตะฝั‹!", + "settingsError": "ะŸั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ ะฟั€ะธ ัะพั…ั€ะฐะฝะตะฝะธะธ ะฝะฐัั‚ั€ะพะตะบ.", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/sk/settings-application.json b/booklore-ui/src/i18n/sk/settings-application.json index 2cd1d0912..6f6dd89f7 100644 --- a/booklore-ui/src/i18n/sk/settings-application.json +++ b/booklore-ui/src/i18n/sk/settings-application.json @@ -1,50 +1,45 @@ { - "title": "Vลกeobecnรฉ nastavenia", - "description": "Nakonfigurujte globรกlne nastavenia pre vaลกu inลกtanciu Booklore, vrรกtane spracovania obrรกzkov obรกlok, preferenciรญ vyhฤพadรกvania a limitov pre nahrรกvanie sรบborov.", - "covers": { - "sectionTitle": "", - "regenerate": "", - "regenerateBtn": "", - "regenerateDesc": "", - "regenerateStarted": "", - "regenerateStartedDetail": "", - "regenerateError": "", - "verticalCropping": "", - "verticalCroppingDesc": "", - "horizontalCropping": "", - "horizontalCroppingDesc": "", - "aspectRatio": "", - "aspectRatioDesc": "", - "smartCropping": "", - "smartCroppingDesc": "" - }, - "search": { - "sectionTitle": "", - "autoBookSearch": "", - "autoBookSearchDesc": "", - "similarBook": "", - "similarBookDesc": "" - }, - "fileManagement": { - "sectionTitle": "", - "maxUploadSize": "", - "maxUploadPlaceholder": "", - "maxUploadDesc": "", - "restartWarning": "", - "invalidInput": "", - "invalidInputDetail": "" - }, - "telemetry": { - "sectionTitle": "", - "enableTelemetry": "", - "telemetryDesc": "" - }, - "settingsSaved": "", - "settingsSavedDetail": "", - "settingsError": "", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "Vลกeobecnรฉ nastavenia", + "description": "Nakonfigurujte globรกlne nastavenia pre vaลกu inลกtanciu Booklore, vrรกtane spracovania obrรกzkov obรกlok, preferenciรญ vyhฤพadรกvania a limitov pre nahrรกvanie sรบborov.", + "covers": { + "sectionTitle": "", + "regenerate": "", + "regenerateBtn": "", + "regenerateDesc": "", + "regenerateStarted": "", + "regenerateStartedDetail": "", + "regenerateError": "", + "verticalCropping": "", + "verticalCroppingDesc": "", + "horizontalCropping": "", + "horizontalCroppingDesc": "", + "aspectRatio": "", + "aspectRatioDesc": "", + "smartCropping": "", + "smartCroppingDesc": "" + }, + "search": { + "sectionTitle": "", + "autoBookSearch": "", + "autoBookSearchDesc": "", + "similarBook": "", + "similarBookDesc": "" + }, + "fileManagement": { + "sectionTitle": "", + "maxUploadSize": "", + "maxUploadPlaceholder": "", + "maxUploadDesc": "", + "restartWarning": "", + "invalidInput": "", + "invalidInputDetail": "" + }, + "settingsSaved": "", + "settingsSavedDetail": "", + "settingsError": "", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/booklore-ui/src/i18n/sl/settings-application.json b/booklore-ui/src/i18n/sl/settings-application.json index 47d1dc123..e8ac808d9 100644 --- a/booklore-ui/src/i18n/sl/settings-application.json +++ b/booklore-ui/src/i18n/sl/settings-application.json @@ -1,52 +1,47 @@ { - "title": "Sploลกne nastavitve", - "description": "Konfiguriraj sploลกne nastavitve za svojo instanco Booklore, vkljuฤno z upravljanjem naslovnic, nastavitvami iskanja in omejitvami nalaganja datotek.", - "covers": { - "sectionTitle": "Naslovnica knjige", - "regenerate": "Ponovno ustvari naslovnice", - "regenerateBtn": "Ponovno ustvari", - "regenerateDesc": "Obnovi slike platnic za vse knjige iz vdelanih platnic v datoteki. Uporabi moลพnost \"Obnovi manjkajoฤe\", da ustvariลก samo platnice za knjige, ki jih ลกe nimajo.", - "regenerateStarted": "Ustvarjanje naslovnic se je zaฤelo", - "regenerateStartedDetail": "Slike naslovnic se trenutno ponovno ustvarjajo.", - "regenerateError": "Ponovnega ustvarjanja naslovnic ni bilo mogoฤe zagnati.", - "verticalCropping": "Navpiฤno obrezovanje naslovnic", - "verticalCroppingDesc": "Samodejno obreลพi izjemno visoke slike (npr. spletni stripi) z vrha, da ustvariลก uporabne sliฤice naslovnic.", - "horizontalCropping": "Vodoravno obrezovanje naslovnic", - "horizontalCroppingDesc": "Samodejno obreลพi izjemno ลกiroke slike z leve strani, da ustvariลก uporabne sliฤice naslovnic.", - "aspectRatio": "Prag razmerja stranic: {{value}}", - "aspectRatioDesc": "Slike, ki presegajo to razmerje stranic, bodo obrezane. Vrednost 2.5 pomeni, da bodo obrezane slike, ki so veฤ kot 2,5-krat viลกje (ali ลกirลกe) od obiฤajnih.", - "smartCropping": "Pametno obrezovanje", - "smartCroppingDesc": "Preskoฤi enobarvne regije pri doloฤanju mesta obreza. Fokusira naslovnico na najbolj relevantno vsebino.", - "regenerateAllBtn": "Obnovi vse", - "regenerateMissingBtn": "Obnovi manjkajoฤe" - }, - "search": { - "sectionTitle": "Iskanje in priporoฤila", - "autoBookSearch": "Samodejno iskanje knjig", - "autoBookSearchDesc": "Samodejno poskusi poiskati ujemajoฤe se metapodatke, ko odpreลก ploลกฤo z informacijami o knjigi.", - "similarBook": "Priporoฤila podobnih knjig", - "similarBookDesc": "Omogoฤi ali onemogoฤi priporoฤila podobnih knjig na podlagi tvoje knjiลพnice." - }, - "fileManagement": { - "sectionTitle": "Upravljanje datotek", - "maxUploadSize": "Najveฤja velikost naloลพene datoteke", - "maxUploadPlaceholder": "Najveฤja velikost", - "maxUploadDesc": "Doloฤa najveฤjo dovoljeno velikost (v MB) za vsako naloลพeno datoteko. Velja za formate EPUB, PDF, CBZ, CBR in CB7.", - "restartWarning": "Spremembe bodo stopile v veljavo po ponovnem zagonu streลพnika", - "invalidInput": "Neveljaven vnos", - "invalidInputDetail": "Prosim, vnesi veljavno najveฤjo velikost datoteke v MB." - }, - "telemetry": { - "sectionTitle": "Telemetrija", - "enableTelemetry": "Omogoฤi telemetrijo", - "telemetryDesc": "Pomagaj izboljลกati Booklore z deljenjem anonimnih statistik uporabe. Ti podatki razvijalcem omogoฤajo vpogled v to, katere funkcije so najbolj uporabljene, prepoznavanje hroลกฤev in teลพav z zmogljivostjo. Osebni podatki, vsebina knjig ali kateri koli drugi identifikacijski podatki se nikoli ne poลกiljajo. Podatki se samodejno poลกljejo na streลพnik Booklore enkrat na 24 ur. To je popolnoma varno in zelo v pomoฤ razvijalcem." - }, - "settingsSaved": "Nastavitve shranjene", - "settingsSavedDetail": "Nastavitve so bile uspeลกno shranjene!", - "settingsError": "Pri shranjevanju nastavitev je priลกlo do napake.", - "appearance": { - "supportButtonAnimation": "Animacija gumba za podporo", - "supportButtonAnimationDesc": "Prikaลพi animirani uฤinek srca na gumbu za podporo v zgornji vrstici. ฤŒe to onemogoฤiลก, gumb ostane viden, vendar se animacija odstrani.", - "sectionTitle": "Izgled" - } + "title": "Sploลกne nastavitve", + "description": "Konfiguriraj sploลกne nastavitve za svojo instanco Booklore, vkljuฤno z upravljanjem naslovnic, nastavitvami iskanja in omejitvami nalaganja datotek.", + "covers": { + "sectionTitle": "Naslovnica knjige", + "regenerate": "Ponovno ustvari naslovnice", + "regenerateBtn": "Ponovno ustvari", + "regenerateDesc": "Obnovi slike platnic za vse knjige iz vdelanih platnic v datoteki. Uporabi moลพnost \"Obnovi manjkajoฤe\", da ustvariลก samo platnice za knjige, ki jih ลกe nimajo.", + "regenerateStarted": "Ustvarjanje naslovnic se je zaฤelo", + "regenerateStartedDetail": "Slike naslovnic se trenutno ponovno ustvarjajo.", + "regenerateError": "Ponovnega ustvarjanja naslovnic ni bilo mogoฤe zagnati.", + "verticalCropping": "Navpiฤno obrezovanje naslovnic", + "verticalCroppingDesc": "Samodejno obreลพi izjemno visoke slike (npr. spletni stripi) z vrha, da ustvariลก uporabne sliฤice naslovnic.", + "horizontalCropping": "Vodoravno obrezovanje naslovnic", + "horizontalCroppingDesc": "Samodejno obreลพi izjemno ลกiroke slike z leve strani, da ustvariลก uporabne sliฤice naslovnic.", + "aspectRatio": "Prag razmerja stranic: {{value}}", + "aspectRatioDesc": "Slike, ki presegajo to razmerje stranic, bodo obrezane. Vrednost 2.5 pomeni, da bodo obrezane slike, ki so veฤ kot 2,5-krat viลกje (ali ลกirลกe) od obiฤajnih.", + "smartCropping": "Pametno obrezovanje", + "smartCroppingDesc": "Preskoฤi enobarvne regije pri doloฤanju mesta obreza. Fokusira naslovnico na najbolj relevantno vsebino.", + "regenerateAllBtn": "Obnovi vse", + "regenerateMissingBtn": "Obnovi manjkajoฤe" + }, + "search": { + "sectionTitle": "Iskanje in priporoฤila", + "autoBookSearch": "Samodejno iskanje knjig", + "autoBookSearchDesc": "Samodejno poskusi poiskati ujemajoฤe se metapodatke, ko odpreลก ploลกฤo z informacijami o knjigi.", + "similarBook": "Priporoฤila podobnih knjig", + "similarBookDesc": "Omogoฤi ali onemogoฤi priporoฤila podobnih knjig na podlagi tvoje knjiลพnice." + }, + "fileManagement": { + "sectionTitle": "Upravljanje datotek", + "maxUploadSize": "Najveฤja velikost naloลพene datoteke", + "maxUploadPlaceholder": "Najveฤja velikost", + "maxUploadDesc": "Doloฤa najveฤjo dovoljeno velikost (v MB) za vsako naloลพeno datoteko. Velja za formate EPUB, PDF, CBZ, CBR in CB7.", + "restartWarning": "Spremembe bodo stopile v veljavo po ponovnem zagonu streลพnika", + "invalidInput": "Neveljaven vnos", + "invalidInputDetail": "Prosim, vnesi veljavno najveฤjo velikost datoteke v MB." + }, + "settingsSaved": "Nastavitve shranjene", + "settingsSavedDetail": "Nastavitve so bile uspeลกno shranjene!", + "settingsError": "Pri shranjevanju nastavitev je priลกlo do napake.", + "appearance": { + "supportButtonAnimation": "Animacija gumba za podporo", + "supportButtonAnimationDesc": "Prikaลพi animirani uฤinek srca na gumbu za podporo v zgornji vrstici. ฤŒe to onemogoฤiลก, gumb ostane viden, vendar se animacija odstrani.", + "sectionTitle": "Izgled" + } } diff --git a/booklore-ui/src/i18n/sv/settings-application.json b/booklore-ui/src/i18n/sv/settings-application.json index eca4ef97b..27d1c3e82 100644 --- a/booklore-ui/src/i18n/sv/settings-application.json +++ b/booklore-ui/src/i18n/sv/settings-application.json @@ -1,52 +1,47 @@ { - "title": "Globala instรคllningar", - "description": "Konfigurera globala instรคllningar fรถr din Booklore-instans, inklusive omslagsbildshantering, sรถkinstรคllningar och filuppladdningsgrรคnser.", - "covers": { - "sectionTitle": "Bokomslag", - "regenerate": "ร…terskapa omslag", - "regenerateBtn": "ร…terskapa", - "regenerateDesc": "ร…terskapar omslagsbilder fรถr alla bรถcker frรฅn de inbรคddade omslagen i filen. Anvรคnd \"Regenerera saknade\" fรถr att bara generera omslag fรถr bรถcker som รคnnu inte har ett.", - "regenerateStarted": "Omslagsรฅterskapning startad", - "regenerateStartedDetail": "Bokomslag hรฅller pรฅ att รฅterskapas.", - "regenerateError": "Kunde inte starta omslagsรฅterskapning.", - "verticalCropping": "Vertikal omslagsbeskรคrning", - "verticalCroppingDesc": "Beskรคr automatiskt extremt hรถga bilder (som webbserier) frรฅn toppen fรถr att skapa anvรคndbara omslagsminiatyrer.", - "horizontalCropping": "Horisontell omslagsbeskรคrning", - "horizontalCroppingDesc": "Beskรคr automatiskt extremt breda bilder frรฅn vรคnster fรถr att skapa anvรคndbara omslagsminiatyrer.", - "aspectRatio": "Bildfรถrhรฅllandestrรถskel: {{value}}", - "aspectRatioDesc": "Bilder med bildfรถrhรฅllanden som รถverstiger detta trรถskelvรคrde beskรคrs. Vรคrdet 2.5 innebรคr att bilder som รคr mer รคn 2,5x hรถgre (eller bredare) รคn normalt beskรคrs.", - "smartCropping": "Smart beskรคrning", - "smartCroppingDesc": "Hoppa รถver enhetliga fรคrgomrรฅden vid bestรคmning av var beskรคrning ska ske. Fokuserar omslagsbilden pรฅ det mest relevanta innehรฅllet.", - "regenerateAllBtn": "Regenerera alla", - "regenerateMissingBtn": "Regenerera saknade" - }, - "search": { - "sectionTitle": "Sรถk och rekommendationer", - "autoBookSearch": "Automatisk boksรถkning", - "autoBookSearchDesc": "Fรถrsรถker automatiskt matcha metadata nรคr bokinformationspanelen รถppnas.", - "similarBook": "Liknande bokrekommendationer", - "similarBookDesc": "Aktiverar eller inaktiverar rekommendationer av liknande bรถcker baserat pรฅ ditt bibliotek." - }, - "fileManagement": { - "sectionTitle": "Filhantering", - "maxUploadSize": "Max filuppladdningsstorlek", - "maxUploadPlaceholder": "Max storlek", - "maxUploadDesc": "Definierar den maximalt tillรฅtna storleken (i MB) fรถr varje uppladdad fil. Gรคller alla filformat som stรถds.", - "restartWarning": "ร„ndringar trรคder i kraft efter omstart av servern", - "invalidInput": "Ogiltig inmatning", - "invalidInputDetail": "Ange en giltig max filuppladdningsstorlek i MB." - }, - "telemetry": { - "sectionTitle": "Telemetri", - "enableTelemetry": "Aktivera telemetri", - "telemetryDesc": "Hjรคlp till att fรถrbรคttra Booklore genom att dela anonym anvรคndningsstatistik. Denna data lรฅter utvecklarna se vilka funktioner som anvรคnds mest, identifiera buggar och upptรคcka prestandaproblem sรฅ att de vet vad de ska arbeta med hรคrnรคst. Ingen personlig information, bokinnehรฅll eller identifierbar data skickas nรฅgonsin. Data skickas till Booklore-servern automatiskt var 24:e timme. Det รคr helt sรคkert och mycket hjรคlpsamt fรถr utvecklarna." - }, - "settingsSaved": "Instรคllningar sparade", - "settingsSavedDetail": "Instรคllningarna sparades!", - "settingsError": "Det uppstod ett fel vid sparning av instรคllningarna.", - "appearance": { - "supportButtonAnimation": "Supportknappsanimation", - "supportButtonAnimationDesc": "Visa den animerade hjรคrteffekten fรถr supportknappen pรฅ รถvre menyraden. Om du inaktiverar detta syns knappen fortfarande men animationen tas bort.", - "sectionTitle": "Utseende" - } + "title": "Globala instรคllningar", + "description": "Konfigurera globala instรคllningar fรถr din Booklore-instans, inklusive omslagsbildshantering, sรถkinstรคllningar och filuppladdningsgrรคnser.", + "covers": { + "sectionTitle": "Bokomslag", + "regenerate": "ร…terskapa omslag", + "regenerateBtn": "ร…terskapa", + "regenerateDesc": "ร…terskapar omslagsbilder fรถr alla bรถcker frรฅn de inbรคddade omslagen i filen. Anvรคnd \"Regenerera saknade\" fรถr att bara generera omslag fรถr bรถcker som รคnnu inte har ett.", + "regenerateStarted": "Omslagsรฅterskapning startad", + "regenerateStartedDetail": "Bokomslag hรฅller pรฅ att รฅterskapas.", + "regenerateError": "Kunde inte starta omslagsรฅterskapning.", + "verticalCropping": "Vertikal omslagsbeskรคrning", + "verticalCroppingDesc": "Beskรคr automatiskt extremt hรถga bilder (som webbserier) frรฅn toppen fรถr att skapa anvรคndbara omslagsminiatyrer.", + "horizontalCropping": "Horisontell omslagsbeskรคrning", + "horizontalCroppingDesc": "Beskรคr automatiskt extremt breda bilder frรฅn vรคnster fรถr att skapa anvรคndbara omslagsminiatyrer.", + "aspectRatio": "Bildfรถrhรฅllandestrรถskel: {{value}}", + "aspectRatioDesc": "Bilder med bildfรถrhรฅllanden som รถverstiger detta trรถskelvรคrde beskรคrs. Vรคrdet 2.5 innebรคr att bilder som รคr mer รคn 2,5x hรถgre (eller bredare) รคn normalt beskรคrs.", + "smartCropping": "Smart beskรคrning", + "smartCroppingDesc": "Hoppa รถver enhetliga fรคrgomrรฅden vid bestรคmning av var beskรคrning ska ske. Fokuserar omslagsbilden pรฅ det mest relevanta innehรฅllet.", + "regenerateAllBtn": "Regenerera alla", + "regenerateMissingBtn": "Regenerera saknade" + }, + "search": { + "sectionTitle": "Sรถk och rekommendationer", + "autoBookSearch": "Automatisk boksรถkning", + "autoBookSearchDesc": "Fรถrsรถker automatiskt matcha metadata nรคr bokinformationspanelen รถppnas.", + "similarBook": "Liknande bokrekommendationer", + "similarBookDesc": "Aktiverar eller inaktiverar rekommendationer av liknande bรถcker baserat pรฅ ditt bibliotek." + }, + "fileManagement": { + "sectionTitle": "Filhantering", + "maxUploadSize": "Max filuppladdningsstorlek", + "maxUploadPlaceholder": "Max storlek", + "maxUploadDesc": "Definierar den maximalt tillรฅtna storleken (i MB) fรถr varje uppladdad fil. Gรคller alla filformat som stรถds.", + "restartWarning": "ร„ndringar trรคder i kraft efter omstart av servern", + "invalidInput": "Ogiltig inmatning", + "invalidInputDetail": "Ange en giltig max filuppladdningsstorlek i MB." + }, + "settingsSaved": "Instรคllningar sparade", + "settingsSavedDetail": "Instรคllningarna sparades!", + "settingsError": "Det uppstod ett fel vid sparning av instรคllningarna.", + "appearance": { + "supportButtonAnimation": "Supportknappsanimation", + "supportButtonAnimationDesc": "Visa den animerade hjรคrteffekten fรถr supportknappen pรฅ รถvre menyraden. Om du inaktiverar detta syns knappen fortfarande men animationen tas bort.", + "sectionTitle": "Utseende" + } } diff --git a/booklore-ui/src/i18n/uk/settings-application.json b/booklore-ui/src/i18n/uk/settings-application.json index 384dde6a1..1ca587f8c 100644 --- a/booklore-ui/src/i18n/uk/settings-application.json +++ b/booklore-ui/src/i18n/uk/settings-application.json @@ -1,52 +1,47 @@ { - "title": "ะ“ะปะพะฑะฐะปัŒะฝั– ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", - "description": "ะะฐะปะฐัˆั‚ัƒะนั‚ะต ะณะปะพะฑะฐะปัŒะฝั– ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฒะฐัˆะพะณะพ ะตะบะทะตะผะฟะปัั€ะฐ Booklore, ะทะพะบั€ะตะผะฐ ะพะฑั€ะพะฑะบัƒ ะพะฑะบะปะฐะดะธะฝะพะบ, ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฟะพัˆัƒะบัƒ ั‚ะฐ ะปั–ะผั–ั‚ะธ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ั„ะฐะนะปั–ะฒ.", - "covers": { - "sectionTitle": "ะžะฑะบะปะฐะดะธะฝะบะฐ ะบะฝะธะณะธ", - "regenerate": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะพะฑะบะปะฐะดะธะฝะบะธ", - "regenerateBtn": "", - "regenerateDesc": "ะŸะตั€ะตะณะตะฝะตั€ะพะฒัƒั” ะพะฑะบะปะฐะดะธะฝะบะธ ะดะปั ะฒัั–ั… ะบะฝะธะณ ั–ะท ะฒะฑัƒะดะพะฒะฐะฝะธั… ัƒ ั„ะฐะนะป ะพะฑะบะปะฐะดะธะฝะพะบ. ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะนั‚ะต \"ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒั–ะดััƒั‚ะฝั–\", ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะพะฑะบะปะฐะดะธะฝะบะธ ะปะธัˆะต ะดะปั ะบะฝะธะณ, ัƒ ัะบะธั… ั—ั… ั‰ะต ะฝะตะผะฐั”.", - "regenerateStarted": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ั–ัŽ ะพะฑะบะปะฐะดะธะฝะพะบ ั€ะพะทะฟะพั‡ะฐั‚ะพ", - "regenerateStartedDetail": "ะžะฑะบะปะฐะดะธะฝะบะธ ะบะฝะธะณ ะฟะตั€ะตะณะตะฝะตั€ะพะฒัƒัŽั‚ัŒัั.", - "regenerateError": "ะะต ะฒะดะฐะปะพัั ะทะฐะฟัƒัั‚ะธั‚ะธ ะฟะตั€ะตะณะตะฝะตั€ะฐั†ั–ัŽ ะพะฑะบะปะฐะดะธะฝะพะบ.", - "verticalCropping": "ะ’ะตั€ั‚ะธะบะฐะปัŒะฝะต ะพะฑั€ั–ะทะฐะฝะฝั ะพะฑะบะปะฐะดะธะฝะพะบ", - "verticalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะพะฑั€ั–ะทะฐั” ะฝะฐะดั‚ะพ ะฒะธัะพะบั– ะทะพะฑั€ะฐะถะตะฝะฝั (ะฝะฐะฟั€ะธะบะปะฐะด, ะฒะตะฑะบะพะผั–ะบัะธ) ะทะฒะตั€ั…ัƒ, ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะฟั€ะธะดะฐั‚ะฝั– ะผั–ะฝั–ะฐั‚ัŽั€ะธ ะพะฑะบะปะฐะดะธะฝะพะบ.", - "horizontalCropping": "ะ“ะพั€ะธะทะพะฝั‚ะฐะปัŒะฝะต ะพะฑั€ั–ะทะฐะฝะฝั ะพะฑะบะปะฐะดะธะฝะพะบ", - "horizontalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะพะฑั€ั–ะทะฐั” ะฝะฐะดั‚ะพ ัˆะธั€ะพะบั– ะทะพะฑั€ะฐะถะตะฝะฝั ะทะปั–ะฒะฐ, ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะฟั€ะธะดะฐั‚ะฝั– ะผั–ะฝั–ะฐั‚ัŽั€ะธ ะพะฑะบะปะฐะดะธะฝะพะบ.", - "aspectRatio": "ะŸะพั€ั–ะณ ัะฟั–ะฒะฒั–ะดะฝะพัˆะตะฝะฝั ัั‚ะพั€ั–ะฝ: {{value}}", - "aspectRatioDesc": "ะ—ะพะฑั€ะฐะถะตะฝะฝั ะทั– ัะฟั–ะฒะฒั–ะดะฝะพัˆะตะฝะฝัะผ ัั‚ะพั€ั–ะฝ, ั‰ะพ ะฟะตั€ะตะฒะธั‰ัƒั” ั†ะตะน ะฟะพั€ั–ะณ, ะฑัƒะดะต ะพะฑั€ั–ะทะฐะฝะพ. ะ—ะฝะฐั‡ะตะฝะฝั 2.5 ะพะทะฝะฐั‡ะฐั”, ั‰ะพ ะทะพะฑั€ะฐะถะตะฝะฝั, ะฒะธั‰ั– (ะฐะฑะพ ัˆะธั€ัˆั–) ะทะฐ ะฝะพั€ะผะฐะปัŒะฝั– ะฑั–ะปัŒัˆ ะฝั–ะถ ัƒ 2.5 ั€ะฐะทะฐ, ะฑัƒะดะต ะพะฑั€ั–ะทะฐะฝะพ.", - "smartCropping": "ะ ะพะทัƒะผะฝะต ะพะฑั€ั–ะทะฐะฝะฝั", - "smartCroppingDesc": "ะŸั€ะพะฟัƒัะบะฐั” ะพะฑะปะฐัั‚ั– ะพะดะฝะพั€ั–ะดะฝะพะณะพ ะบะพะปัŒะพั€ัƒ ะฟั–ะด ั‡ะฐั ะฒะธะทะฝะฐั‡ะตะฝะฝั ะผั–ัั†ั ะพะฑั€ั–ะทะฐะฝะฝั. ะคะพะบัƒััƒั” ะพะฑะบะปะฐะดะธะฝะบัƒ ะฝะฐ ะฝะฐะนั€ะตะปะตะฒะฐะฝั‚ะฝั–ัˆะพะผัƒ ะฒะผั–ัั‚ั–.", - "regenerateAllBtn": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒัั–", - "regenerateMissingBtn": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒั–ะดััƒั‚ะฝั–" - }, - "search": { - "sectionTitle": "ะŸะพัˆัƒะบ ั– ั€ะตะบะพะผะตะฝะดะฐั†ั–ั—", - "autoBookSearch": "ะะฒั‚ะพะฟะพัˆัƒะบ ะบะฝะธะณะธ", - "autoBookSearchDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะฝะฐะผะฐะณะฐั”ั‚ัŒัั ะทั–ัั‚ะฐะฒะธั‚ะธ ะผะตั‚ะฐะดะฐะฝั–, ะบะพะปะธ ะฒั–ะดะบั€ะธั‚ะพ ะฟะฐะฝะตะปัŒ ั–ะฝั„ะพั€ะผะฐั†ั–ั— ะฟั€ะพ ะบะฝะธะณัƒ.", - "similarBook": "ะ ะตะบะพะผะตะฝะดะฐั†ั–ั— ัั…ะพะถะธั… ะบะฝะธะณ", - "similarBookDesc": "ะ’ะผะธะบะฐั” ะฐะฑะพ ะฒะธะผะธะบะฐั” ั€ะตะบะพะผะตะฝะดะฐั†ั–ั— ัั…ะพะถะธั… ะบะฝะธะณ ะฝะฐ ะพัะฝะพะฒั– ะฒะฐัˆะพั— ะฑั–ะฑะปั–ะพั‚ะตะบะธ." - }, - "fileManagement": { - "sectionTitle": "ะšะตั€ัƒะฒะฐะฝะฝั ั„ะฐะนะปะฐะผะธ", - "maxUploadSize": "ะœะฐะบัะธะผะฐะปัŒะฝะธะน ั€ะพะทะผั–ั€ ั„ะฐะนะปัƒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั", - "maxUploadPlaceholder": "ะœะฐะบั. ั€ะพะทะผั–ั€", - "maxUploadDesc": "ะ’ะธะทะฝะฐั‡ะฐั” ะผะฐะบัะธะผะฐะปัŒะฝะพ ะดะพะทะฒะพะปะตะฝะธะน ั€ะพะทะผั–ั€ (ัƒ ะœะ‘) ะดะปั ะบะพะถะฝะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐะฝะพะณะพ ั„ะฐะนะปัƒ. ะ—ะฐัั‚ะพัะพะฒัƒั”ั‚ัŒัั ะดะพ ะฒัั–ั… ะฟั–ะดั‚ั€ะธะผัƒะฒะฐะฝะธั… ั„ะพั€ะผะฐั‚ั–ะฒ.", - "restartWarning": "ะ—ะผั–ะฝะธ ะฝะฐะฑัƒะดัƒั‚ัŒ ั‡ะธะฝะฝะพัั‚ั– ะฟั–ัะปั ะฟะตั€ะตะทะฐะฟัƒัะบัƒ ัะตั€ะฒะตั€ะฐ", - "invalidInput": "ะะตะบะพั€ะตะบั‚ะฝะต ะทะฝะฐั‡ะตะฝะฝั", - "invalidInputDetail": "ะ’ะฒะตะดั–ั‚ัŒ ะบะพั€ะตะบั‚ะฝะธะน ะผะฐะบัะธะผะฐะปัŒะฝะธะน ั€ะพะทะผั–ั€ ั„ะฐะนะปัƒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะฒ ะœะ‘." - }, - "appearance": { - "sectionTitle": "ะ—ะพะฒะฝั–ัˆะฝั–ะน ะฒะธะณะปัะด", - "supportButtonAnimation": "ะะฝั–ะผะฐั†ั–ั ะบะฝะพะฟะบะธ ะฟั–ะดั‚ั€ะธะผะบะธ", - "supportButtonAnimationDesc": "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะฐะฝั–ะผะพะฒะฐะฝะธะน ะตั„ะตะบั‚ ัะตั€ั†ั ะฝะฐ ะบะฝะพะฟั†ั– ะฟั–ะดั‚ั€ะธะผะบะธ ัƒ ะฒะตั€ั…ะฝั–ะน ะฟะฐะฝะตะปั–. ะ’ะธะผะบะฝะตะฝะฝั ะทะฐะปะธัˆะฐั” ะบะฝะพะฟะบัƒ ะฒะธะดะธะผะพัŽ, ะฐะปะต ะฟั€ะธะฑะธั€ะฐั” ะฐะฝั–ะผะฐั†ั–ัŽ." - }, - "telemetry": { - "sectionTitle": "ะขะตะปะตะผะตั‚ั€ั–ั", - "enableTelemetry": "ะฃะฒั–ะผะบะฝัƒั‚ะธ ั‚ะตะปะตะผะตั‚ั€ั–ัŽ", - "telemetryDesc": "ะ”ะพะฟะพะผะพะถั–ั‚ัŒ ะฟะพะบั€ะฐั‰ัƒะฒะฐั‚ะธ Booklore, ะฝะฐะดัะธะปะฐัŽั‡ะธ ะฐะฝะพะฝั–ะผะฝัƒ ัั‚ะฐั‚ะธัั‚ะธะบัƒ ะฒะธะบะพั€ะธัั‚ะฐะฝะฝั. ะฆั– ะดะฐะฝั– ะดะฐัŽั‚ัŒ ะทะผะพะณัƒ ั€ะพะทั€ะพะฑะฝะธะบะฐะผ ะฑะฐั‡ะธั‚ะธ, ัะบั– ั„ัƒะฝะบั†ั–ั— ะฝะฐะนั‡ะฐัั‚ั–ัˆะต ะฒะธะบะพั€ะธัั‚ะพะฒัƒัŽั‚ัŒัั, ะฒะธัะฒะปัั‚ะธ ะฟะพะผะธะปะบะธ ั‚ะฐ ะฟั€ะพะฑะปะตะผะธ ะฟั€ะพะดัƒะบั‚ะธะฒะฝะพัั‚ั–, ั‰ะพะฑ ั€ะพะทัƒะผั–ั‚ะธ, ะฝะฐะด ั‡ะธะผ ะฟั€ะฐั†ัŽะฒะฐั‚ะธ ะดะฐะปั–. ะ–ะพะดะฝะฐ ะพัะพะฑะธัั‚ะฐ ั–ะฝั„ะพั€ะผะฐั†ั–ั, ะฒะผั–ัั‚ ะบะฝะธะณ ะฐะฑะพ ะฑัƒะดัŒ-ัะบั– ั–ะดะตะฝั‚ะธั„ั–ะบะฐั†ั–ะนะฝั– ะดะฐะฝั– ะฝั–ะบะพะปะธ ะฝะต ะฝะฐะดัะธะปะฐัŽั‚ัŒัั. ะ”ะฐะฝั– ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะฝะฐะดัะธะปะฐัŽั‚ัŒัั ะฝะฐ ัะตั€ะฒะตั€ Booklore ั€ะฐะท ะฝะฐ 24 ะณะพะดะธะฝะธ. ะฆะต ะฟะพะฒะฝั–ัั‚ัŽ ะฑะตะทะฟะตั‡ะฝะพ ั– ะดัƒะถะต ะบะพั€ะธัะฝะพ ะดะปั ั€ะพะทั€ะพะฑะฝะธะบั–ะฒ." - }, - "settingsSaved": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะทะฑะตั€ะตะถะตะฝะพ", - "settingsSavedDetail": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ัƒัะฟั–ัˆะฝะพ ะทะฑะตั€ะตะถะตะฝะพ!", - "settingsError": "ะŸั–ะด ั‡ะฐั ะทะฑะตั€ะตะถะตะฝะฝั ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝัŒ ัั‚ะฐะปะฐัั ะฟะพะผะธะปะบะฐ." + "title": "ะ“ะปะพะฑะฐะปัŒะฝั– ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", + "description": "ะะฐะปะฐัˆั‚ัƒะนั‚ะต ะณะปะพะฑะฐะปัŒะฝั– ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฒะฐัˆะพะณะพ ะตะบะทะตะผะฟะปัั€ะฐ Booklore, ะทะพะบั€ะตะผะฐ ะพะฑั€ะพะฑะบัƒ ะพะฑะบะปะฐะดะธะฝะพะบ, ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฟะพัˆัƒะบัƒ ั‚ะฐ ะปั–ะผั–ั‚ะธ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ั„ะฐะนะปั–ะฒ.", + "covers": { + "sectionTitle": "ะžะฑะบะปะฐะดะธะฝะบะฐ ะบะฝะธะณะธ", + "regenerate": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะพะฑะบะปะฐะดะธะฝะบะธ", + "regenerateBtn": "", + "regenerateDesc": "ะŸะตั€ะตะณะตะฝะตั€ะพะฒัƒั” ะพะฑะบะปะฐะดะธะฝะบะธ ะดะปั ะฒัั–ั… ะบะฝะธะณ ั–ะท ะฒะฑัƒะดะพะฒะฐะฝะธั… ัƒ ั„ะฐะนะป ะพะฑะบะปะฐะดะธะฝะพะบ. ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะนั‚ะต \"ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒั–ะดััƒั‚ะฝั–\", ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะพะฑะบะปะฐะดะธะฝะบะธ ะปะธัˆะต ะดะปั ะบะฝะธะณ, ัƒ ัะบะธั… ั—ั… ั‰ะต ะฝะตะผะฐั”.", + "regenerateStarted": "ะŸะตั€ะตะณะตะฝะตั€ะฐั†ั–ัŽ ะพะฑะบะปะฐะดะธะฝะพะบ ั€ะพะทะฟะพั‡ะฐั‚ะพ", + "regenerateStartedDetail": "ะžะฑะบะปะฐะดะธะฝะบะธ ะบะฝะธะณ ะฟะตั€ะตะณะตะฝะตั€ะพะฒัƒัŽั‚ัŒัั.", + "regenerateError": "ะะต ะฒะดะฐะปะพัั ะทะฐะฟัƒัั‚ะธั‚ะธ ะฟะตั€ะตะณะตะฝะตั€ะฐั†ั–ัŽ ะพะฑะบะปะฐะดะธะฝะพะบ.", + "verticalCropping": "ะ’ะตั€ั‚ะธะบะฐะปัŒะฝะต ะพะฑั€ั–ะทะฐะฝะฝั ะพะฑะบะปะฐะดะธะฝะพะบ", + "verticalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะพะฑั€ั–ะทะฐั” ะฝะฐะดั‚ะพ ะฒะธัะพะบั– ะทะพะฑั€ะฐะถะตะฝะฝั (ะฝะฐะฟั€ะธะบะปะฐะด, ะฒะตะฑะบะพะผั–ะบัะธ) ะทะฒะตั€ั…ัƒ, ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะฟั€ะธะดะฐั‚ะฝั– ะผั–ะฝั–ะฐั‚ัŽั€ะธ ะพะฑะบะปะฐะดะธะฝะพะบ.", + "horizontalCropping": "ะ“ะพั€ะธะทะพะฝั‚ะฐะปัŒะฝะต ะพะฑั€ั–ะทะฐะฝะฝั ะพะฑะบะปะฐะดะธะฝะพะบ", + "horizontalCroppingDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะพะฑั€ั–ะทะฐั” ะฝะฐะดั‚ะพ ัˆะธั€ะพะบั– ะทะพะฑั€ะฐะถะตะฝะฝั ะทะปั–ะฒะฐ, ั‰ะพะฑ ัั‚ะฒะพั€ะธั‚ะธ ะฟั€ะธะดะฐั‚ะฝั– ะผั–ะฝั–ะฐั‚ัŽั€ะธ ะพะฑะบะปะฐะดะธะฝะพะบ.", + "aspectRatio": "ะŸะพั€ั–ะณ ัะฟั–ะฒะฒั–ะดะฝะพัˆะตะฝะฝั ัั‚ะพั€ั–ะฝ: {{value}}", + "aspectRatioDesc": "ะ—ะพะฑั€ะฐะถะตะฝะฝั ะทั– ัะฟั–ะฒะฒั–ะดะฝะพัˆะตะฝะฝัะผ ัั‚ะพั€ั–ะฝ, ั‰ะพ ะฟะตั€ะตะฒะธั‰ัƒั” ั†ะตะน ะฟะพั€ั–ะณ, ะฑัƒะดะต ะพะฑั€ั–ะทะฐะฝะพ. ะ—ะฝะฐั‡ะตะฝะฝั 2.5 ะพะทะฝะฐั‡ะฐั”, ั‰ะพ ะทะพะฑั€ะฐะถะตะฝะฝั, ะฒะธั‰ั– (ะฐะฑะพ ัˆะธั€ัˆั–) ะทะฐ ะฝะพั€ะผะฐะปัŒะฝั– ะฑั–ะปัŒัˆ ะฝั–ะถ ัƒ 2.5 ั€ะฐะทะฐ, ะฑัƒะดะต ะพะฑั€ั–ะทะฐะฝะพ.", + "smartCropping": "ะ ะพะทัƒะผะฝะต ะพะฑั€ั–ะทะฐะฝะฝั", + "smartCroppingDesc": "ะŸั€ะพะฟัƒัะบะฐั” ะพะฑะปะฐัั‚ั– ะพะดะฝะพั€ั–ะดะฝะพะณะพ ะบะพะปัŒะพั€ัƒ ะฟั–ะด ั‡ะฐั ะฒะธะทะฝะฐั‡ะตะฝะฝั ะผั–ัั†ั ะพะฑั€ั–ะทะฐะฝะฝั. ะคะพะบัƒััƒั” ะพะฑะบะปะฐะดะธะฝะบัƒ ะฝะฐ ะฝะฐะนั€ะตะปะตะฒะฐะฝั‚ะฝั–ัˆะพะผัƒ ะฒะผั–ัั‚ั–.", + "regenerateAllBtn": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒัั–", + "regenerateMissingBtn": "ะŸะตั€ะตะณะตะฝะตั€ัƒะฒะฐั‚ะธ ะฒั–ะดััƒั‚ะฝั–" + }, + "search": { + "sectionTitle": "ะŸะพัˆัƒะบ ั– ั€ะตะบะพะผะตะฝะดะฐั†ั–ั—", + "autoBookSearch": "ะะฒั‚ะพะฟะพัˆัƒะบ ะบะฝะธะณะธ", + "autoBookSearchDesc": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ ะฝะฐะผะฐะณะฐั”ั‚ัŒัั ะทั–ัั‚ะฐะฒะธั‚ะธ ะผะตั‚ะฐะดะฐะฝั–, ะบะพะปะธ ะฒั–ะดะบั€ะธั‚ะพ ะฟะฐะฝะตะปัŒ ั–ะฝั„ะพั€ะผะฐั†ั–ั— ะฟั€ะพ ะบะฝะธะณัƒ.", + "similarBook": "ะ ะตะบะพะผะตะฝะดะฐั†ั–ั— ัั…ะพะถะธั… ะบะฝะธะณ", + "similarBookDesc": "ะ’ะผะธะบะฐั” ะฐะฑะพ ะฒะธะผะธะบะฐั” ั€ะตะบะพะผะตะฝะดะฐั†ั–ั— ัั…ะพะถะธั… ะบะฝะธะณ ะฝะฐ ะพัะฝะพะฒั– ะฒะฐัˆะพั— ะฑั–ะฑะปั–ะพั‚ะตะบะธ." + }, + "fileManagement": { + "sectionTitle": "ะšะตั€ัƒะฒะฐะฝะฝั ั„ะฐะนะปะฐะผะธ", + "maxUploadSize": "ะœะฐะบัะธะผะฐะปัŒะฝะธะน ั€ะพะทะผั–ั€ ั„ะฐะนะปัƒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั", + "maxUploadPlaceholder": "ะœะฐะบั. ั€ะพะทะผั–ั€", + "maxUploadDesc": "ะ’ะธะทะฝะฐั‡ะฐั” ะผะฐะบัะธะผะฐะปัŒะฝะพ ะดะพะทะฒะพะปะตะฝะธะน ั€ะพะทะผั–ั€ (ัƒ ะœะ‘) ะดะปั ะบะพะถะฝะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐะฝะพะณะพ ั„ะฐะนะปัƒ. ะ—ะฐัั‚ะพัะพะฒัƒั”ั‚ัŒัั ะดะพ ะฒัั–ั… ะฟั–ะดั‚ั€ะธะผัƒะฒะฐะฝะธั… ั„ะพั€ะผะฐั‚ั–ะฒ.", + "restartWarning": "ะ—ะผั–ะฝะธ ะฝะฐะฑัƒะดัƒั‚ัŒ ั‡ะธะฝะฝะพัั‚ั– ะฟั–ัะปั ะฟะตั€ะตะทะฐะฟัƒัะบัƒ ัะตั€ะฒะตั€ะฐ", + "invalidInput": "ะะตะบะพั€ะตะบั‚ะฝะต ะทะฝะฐั‡ะตะฝะฝั", + "invalidInputDetail": "ะ’ะฒะตะดั–ั‚ัŒ ะบะพั€ะตะบั‚ะฝะธะน ะผะฐะบัะธะผะฐะปัŒะฝะธะน ั€ะพะทะผั–ั€ ั„ะฐะนะปัƒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะฒ ะœะ‘." + }, + "appearance": { + "sectionTitle": "ะ—ะพะฒะฝั–ัˆะฝั–ะน ะฒะธะณะปัะด", + "supportButtonAnimation": "ะะฝั–ะผะฐั†ั–ั ะบะฝะพะฟะบะธ ะฟั–ะดั‚ั€ะธะผะบะธ", + "supportButtonAnimationDesc": "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะฐะฝั–ะผะพะฒะฐะฝะธะน ะตั„ะตะบั‚ ัะตั€ั†ั ะฝะฐ ะบะฝะพะฟั†ั– ะฟั–ะดั‚ั€ะธะผะบะธ ัƒ ะฒะตั€ั…ะฝั–ะน ะฟะฐะฝะตะปั–. ะ’ะธะผะบะฝะตะฝะฝั ะทะฐะปะธัˆะฐั” ะบะฝะพะฟะบัƒ ะฒะธะดะธะผะพัŽ, ะฐะปะต ะฟั€ะธะฑะธั€ะฐั” ะฐะฝั–ะผะฐั†ั–ัŽ." + }, + "settingsSaved": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะทะฑะตั€ะตะถะตะฝะพ", + "settingsSavedDetail": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ัƒัะฟั–ัˆะฝะพ ะทะฑะตั€ะตะถะตะฝะพ!", + "settingsError": "ะŸั–ะด ั‡ะฐั ะทะฑะตั€ะตะถะตะฝะฝั ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝัŒ ัั‚ะฐะปะฐัั ะฟะพะผะธะปะบะฐ." } diff --git a/booklore-ui/src/i18n/zh/settings-application.json b/booklore-ui/src/i18n/zh/settings-application.json index 2f546beaf..2069d4654 100644 --- a/booklore-ui/src/i18n/zh/settings-application.json +++ b/booklore-ui/src/i18n/zh/settings-application.json @@ -1,50 +1,45 @@ { - "title": "ๅ…จๅฑ€ๅๅฅฝ่ฎพ็ฝฎ", - "description": "้…็ฝฎ BookLore ๅฎžไพ‹็š„ๅ…จๅฑ€่ฎพ็ฝฎ๏ผŒๅŒ…ๆ‹ฌๅฐ้ขๅ›พ็‰‡ๅค„็†ใ€ๆœ็ดขๅๅฅฝๅ’Œๆ–‡ไปถไธŠไผ ้™ๅˆถใ€‚", - "covers": { - "sectionTitle": "ไนฆ็ฑๅฐ้ขๅ›พ็‰‡", - "regenerate": "้‡ๆ–ฐ็”Ÿๆˆๅฐ้ข", - "regenerateBtn": "้‡ๆ–ฐ็”Ÿๆˆ", - "regenerateDesc": "ไปŽๆ–‡ไปถไธญๅตŒๅ…ฅ็š„ๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๆ‰€ๆœ‰ไนฆ็ฑ็š„ๅฐ้ขๅ›พ็‰‡ใ€‚", - "regenerateStarted": "ๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๅทฒๅผ€ๅง‹", - "regenerateStartedDetail": "ๆญฃๅœจ้‡ๆ–ฐ็”Ÿๆˆไนฆ็ฑๅฐ้ขใ€‚", - "regenerateError": "ๅฏๅŠจๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๅคฑ่ดฅใ€‚", - "verticalCropping": "ๅž‚็›ดๅฐ้ข่ฃๅ‰ช", - "verticalCroppingDesc": "่‡ชๅŠจไปŽ้กถ้ƒจ่ฃๅ‰ช่ถ…้ซ˜ๅ›พ็‰‡๏ผˆๅฆ‚็ฝ‘็ปœๆผซ็”ป๏ผ‰๏ผŒไปฅๅˆ›ๅปบๅฏ็”จ็š„ๅฐ้ข็ผฉ็•ฅๅ›พใ€‚", - "horizontalCropping": "ๆฐดๅนณๅฐ้ข่ฃๅ‰ช", - "horizontalCroppingDesc": "่‡ชๅŠจไปŽๅทฆไพง่ฃๅ‰ช่ถ…ๅฎฝๅ›พ็‰‡๏ผŒไปฅๅˆ›ๅปบๅฏ็”จ็š„ๅฐ้ข็ผฉ็•ฅๅ›พใ€‚", - "aspectRatio": "ๅฎฝ้ซ˜ๆฏ”้˜ˆๅ€ผ๏ผš{{value}}", - "aspectRatioDesc": "ๅฎฝ้ซ˜ๆฏ”่ถ…่ฟ‡ๆญค้˜ˆๅ€ผ็š„ๅ›พ็‰‡ๅฐ†่ขซ่ฃๅ‰ชใ€‚ๅ€ผไธบ 2.5 ่กจ็คบ้ซ˜ๅบฆ๏ผˆๆˆ–ๅฎฝๅบฆ๏ผ‰่ถ…่ฟ‡ๆญฃๅธธๅ€ผ 2.5 ๅ€็š„ๅ›พ็‰‡ๅฐ†่ขซ่ฃๅ‰ชใ€‚", - "smartCropping": "ๆ™บ่ƒฝ่ฃๅ‰ช", - "smartCroppingDesc": "ๅœจ็กฎๅฎš่ฃๅ‰ชไฝ็ฝฎๆ—ถ่ทณ่ฟ‡็บฏ่‰ฒๅŒบๅŸŸ๏ผŒๅฐ†ๅฐ้ขๅ›พ็‰‡่š็„ฆๅœจๆœ€็›ธๅ…ณ็š„ๅ†…ๅฎนไธŠใ€‚" - }, - "search": { - "sectionTitle": "ๆœ็ดขไธŽๆŽจ่", - "autoBookSearch": "่‡ชๅŠจไนฆ็ฑๆœ็ดข", - "autoBookSearchDesc": "ๆ‰“ๅผ€ไนฆ็ฑไฟกๆฏ้ขๆฟๆ—ถ่‡ชๅŠจๅฐ่ฏ•ๅ…ƒๆ•ฐๆฎๅŒน้…ใ€‚", - "similarBook": "็›ธไผผไนฆ็ฑๆŽจ่", - "similarBookDesc": "ๅฏ็”จๆˆ–็ฆ็”จๅŸบไบŽๆ‚จไนฆๅบ“็š„็›ธไผผไนฆ็ฑๆŽจ่ใ€‚" - }, - "fileManagement": { - "sectionTitle": "ๆ–‡ไปถ็ฎก็†", - "maxUploadSize": "ๆœ€ๅคงๆ–‡ไปถไธŠไผ ๅคงๅฐ", - "maxUploadPlaceholder": "ๆœ€ๅคงๅคงๅฐ", - "maxUploadDesc": "ๅฎšไน‰ๆฏไธชไธŠไผ ๆ–‡ไปถ็š„ๆœ€ๅคงๅ…่ฎธๅคงๅฐ๏ผˆไปฅ MB ไธบๅ•ไฝ๏ผ‰ใ€‚้€‚็”จไบŽๆ‰€ๆœ‰ๆ”ฏๆŒ็š„ๆ ผๅผใ€‚", - "restartWarning": "ๆ›ดๆ”นๅฐ†ๅœจ้‡ๅฏๆœๅŠกๅ™จๅŽ็”Ÿๆ•ˆ", - "invalidInput": "ๆ— ๆ•ˆ่พ“ๅ…ฅ", - "invalidInputDetail": "่ฏท่พ“ๅ…ฅๆœ‰ๆ•ˆ็š„ๆœ€ๅคงๆ–‡ไปถไธŠไผ ๅคงๅฐ๏ผˆไปฅ MB ไธบๅ•ไฝ๏ผ‰ใ€‚" - }, - "telemetry": { - "sectionTitle": "้ฅๆต‹", - "enableTelemetry": "ๅฏ็”จ้ฅๆต‹", - "telemetryDesc": "้€š่ฟ‡ๅ…ฑไบซๅŒฟๅไฝฟ็”จ็ปŸ่ฎกๆ•ฐๆฎๆฅๅธฎๅŠฉๆ”น่ฟ› BookLoreใ€‚่ฟ™ไบ›ๆ•ฐๆฎๅฏไปฅ่ฎฉๅผ€ๅ‘่€…ไบ†่งฃๅ“ชไบ›ๅŠŸ่ƒฝๆœ€ๅธธไฝฟ็”จใ€่ฏ†ๅˆซ้”™่ฏฏๅ’Œๅ‘็Žฐๆ€ง่ƒฝ้—ฎ้ข˜๏ผŒไปŽ่€Œ็Ÿฅ้“ไธ‹ไธ€ๆญฅ่ฏฅๅšไป€ไนˆใ€‚ไธไผšๅ‘้€ไปปไฝ•ไธชไบบไฟกๆฏใ€ไนฆ็ฑๅ†…ๅฎนๆˆ–ไปปไฝ•ๅฏ่ฏ†ๅˆซ็š„ๆ•ฐๆฎใ€‚ๆ•ฐๆฎๆฏ 24 ๅฐๆ—ถ่‡ชๅŠจๅ‘้€ไธ€ๆฌกๅˆฐ BookLore ๆœๅŠกๅ™จใ€‚่ฟ™ๅฎŒๅ…จๅฎ‰ๅ…จ๏ผŒๅฏนๅผ€ๅ‘่€…้žๅธธๆœ‰ๅธฎๅŠฉใ€‚" - }, - "settingsSaved": "่ฎพ็ฝฎๅทฒไฟๅญ˜", - "settingsSavedDetail": "่ฎพ็ฝฎๅทฒๆˆๅŠŸไฟๅญ˜๏ผ", - "settingsError": "ไฟๅญ˜่ฎพ็ฝฎๆ—ถๅ‡บ้”™ใ€‚", - "appearance": { - "supportButtonAnimation": "", - "supportButtonAnimationDesc": "", - "sectionTitle": "" - } + "title": "ๅ…จๅฑ€ๅๅฅฝ่ฎพ็ฝฎ", + "description": "้…็ฝฎ BookLore ๅฎžไพ‹็š„ๅ…จๅฑ€่ฎพ็ฝฎ๏ผŒๅŒ…ๆ‹ฌๅฐ้ขๅ›พ็‰‡ๅค„็†ใ€ๆœ็ดขๅๅฅฝๅ’Œๆ–‡ไปถไธŠไผ ้™ๅˆถใ€‚", + "covers": { + "sectionTitle": "ไนฆ็ฑๅฐ้ขๅ›พ็‰‡", + "regenerate": "้‡ๆ–ฐ็”Ÿๆˆๅฐ้ข", + "regenerateBtn": "้‡ๆ–ฐ็”Ÿๆˆ", + "regenerateDesc": "ไปŽๆ–‡ไปถไธญๅตŒๅ…ฅ็š„ๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๆ‰€ๆœ‰ไนฆ็ฑ็š„ๅฐ้ขๅ›พ็‰‡ใ€‚", + "regenerateStarted": "ๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๅทฒๅผ€ๅง‹", + "regenerateStartedDetail": "ๆญฃๅœจ้‡ๆ–ฐ็”Ÿๆˆไนฆ็ฑๅฐ้ขใ€‚", + "regenerateError": "ๅฏๅŠจๅฐ้ข้‡ๆ–ฐ็”Ÿๆˆๅคฑ่ดฅใ€‚", + "verticalCropping": "ๅž‚็›ดๅฐ้ข่ฃๅ‰ช", + "verticalCroppingDesc": "่‡ชๅŠจไปŽ้กถ้ƒจ่ฃๅ‰ช่ถ…้ซ˜ๅ›พ็‰‡๏ผˆๅฆ‚็ฝ‘็ปœๆผซ็”ป๏ผ‰๏ผŒไปฅๅˆ›ๅปบๅฏ็”จ็š„ๅฐ้ข็ผฉ็•ฅๅ›พใ€‚", + "horizontalCropping": "ๆฐดๅนณๅฐ้ข่ฃๅ‰ช", + "horizontalCroppingDesc": "่‡ชๅŠจไปŽๅทฆไพง่ฃๅ‰ช่ถ…ๅฎฝๅ›พ็‰‡๏ผŒไปฅๅˆ›ๅปบๅฏ็”จ็š„ๅฐ้ข็ผฉ็•ฅๅ›พใ€‚", + "aspectRatio": "ๅฎฝ้ซ˜ๆฏ”้˜ˆๅ€ผ๏ผš{{value}}", + "aspectRatioDesc": "ๅฎฝ้ซ˜ๆฏ”่ถ…่ฟ‡ๆญค้˜ˆๅ€ผ็š„ๅ›พ็‰‡ๅฐ†่ขซ่ฃๅ‰ชใ€‚ๅ€ผไธบ 2.5 ่กจ็คบ้ซ˜ๅบฆ๏ผˆๆˆ–ๅฎฝๅบฆ๏ผ‰่ถ…่ฟ‡ๆญฃๅธธๅ€ผ 2.5 ๅ€็š„ๅ›พ็‰‡ๅฐ†่ขซ่ฃๅ‰ชใ€‚", + "smartCropping": "ๆ™บ่ƒฝ่ฃๅ‰ช", + "smartCroppingDesc": "ๅœจ็กฎๅฎš่ฃๅ‰ชไฝ็ฝฎๆ—ถ่ทณ่ฟ‡็บฏ่‰ฒๅŒบๅŸŸ๏ผŒๅฐ†ๅฐ้ขๅ›พ็‰‡่š็„ฆๅœจๆœ€็›ธๅ…ณ็š„ๅ†…ๅฎนไธŠใ€‚" + }, + "search": { + "sectionTitle": "ๆœ็ดขไธŽๆŽจ่", + "autoBookSearch": "่‡ชๅŠจไนฆ็ฑๆœ็ดข", + "autoBookSearchDesc": "ๆ‰“ๅผ€ไนฆ็ฑไฟกๆฏ้ขๆฟๆ—ถ่‡ชๅŠจๅฐ่ฏ•ๅ…ƒๆ•ฐๆฎๅŒน้…ใ€‚", + "similarBook": "็›ธไผผไนฆ็ฑๆŽจ่", + "similarBookDesc": "ๅฏ็”จๆˆ–็ฆ็”จๅŸบไบŽๆ‚จไนฆๅบ“็š„็›ธไผผไนฆ็ฑๆŽจ่ใ€‚" + }, + "fileManagement": { + "sectionTitle": "ๆ–‡ไปถ็ฎก็†", + "maxUploadSize": "ๆœ€ๅคงๆ–‡ไปถไธŠไผ ๅคงๅฐ", + "maxUploadPlaceholder": "ๆœ€ๅคงๅคงๅฐ", + "maxUploadDesc": "ๅฎšไน‰ๆฏไธชไธŠไผ ๆ–‡ไปถ็š„ๆœ€ๅคงๅ…่ฎธๅคงๅฐ๏ผˆไปฅ MB ไธบๅ•ไฝ๏ผ‰ใ€‚้€‚็”จไบŽๆ‰€ๆœ‰ๆ”ฏๆŒ็š„ๆ ผๅผใ€‚", + "restartWarning": "ๆ›ดๆ”นๅฐ†ๅœจ้‡ๅฏๆœๅŠกๅ™จๅŽ็”Ÿๆ•ˆ", + "invalidInput": "ๆ— ๆ•ˆ่พ“ๅ…ฅ", + "invalidInputDetail": "่ฏท่พ“ๅ…ฅๆœ‰ๆ•ˆ็š„ๆœ€ๅคงๆ–‡ไปถไธŠไผ ๅคงๅฐ๏ผˆไปฅ MB ไธบๅ•ไฝ๏ผ‰ใ€‚" + }, + "settingsSaved": "่ฎพ็ฝฎๅทฒไฟๅญ˜", + "settingsSavedDetail": "่ฎพ็ฝฎๅทฒๆˆๅŠŸไฟๅญ˜๏ผ", + "settingsError": "ไฟๅญ˜่ฎพ็ฝฎๆ—ถๅ‡บ้”™ใ€‚", + "appearance": { + "supportButtonAnimation": "", + "supportButtonAnimationDesc": "", + "sectionTitle": "" + } } diff --git a/docs/OIDC-Setup-With-PocketID.md b/docs/OIDC-Setup-With-PocketID.md index 154c9c40c..450b8041d 100644 --- a/docs/OIDC-Setup-With-PocketID.md +++ b/docs/OIDC-Setup-With-PocketID.md @@ -53,4 +53,4 @@ ### Step 3: Configure BookLore OIDC Settings ### Step 4: Test the Integration -Once configured, simply click Save Settings and then click the Enabled radio button to activate it. Simply log out and log back in and you should be working with no issues! Any issues please raise an issue on the [Github](https://github.com/booklore-app/booklore/issues/new?template=bug_report.yml) +Once configured, simply click Save Settings and then click the Enabled radio button to activate it. Simply log out and log back in and you should be working with no issues! Any issues please raise an issue on the [Github](https://github.com/the-booklore/booklore/issues/new?template=bug_report.yml) diff --git a/docs/forward-auth-with-proxy.md b/docs/forward-auth-with-proxy.md index 93af1b693..c1667026d 100644 --- a/docs/forward-auth-with-proxy.md +++ b/docs/forward-auth-with-proxy.md @@ -37,7 +37,7 @@ ### Docker Compose Example ```yaml services: booklore: - image: ghcr.io/adityachandelgit/booklore-app:latest + image: ghcr.io/the-booklore/booklore:latest environment: # Forward Auth Configuration - REMOTE_AUTH_ENABLED=true diff --git a/example-chart/Chart.yaml b/example-chart/Chart.yaml index 271eefc8b..8ae93350c 100644 --- a/example-chart/Chart.yaml +++ b/example-chart/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: booklore -description: A Helm chart for [Booklore](https://github.com/booklore-app/booklore) +description: A Helm chart for [Booklore](https://github.com/the-booklore/booklore) # A chart can be either an 'application' or a 'library' chart. # diff --git a/example-chart/values.yaml b/example-chart/values.yaml index cd02f91bd..b20ef97a8 100644 --- a/example-chart/values.yaml +++ b/example-chart/values.yaml @@ -12,7 +12,7 @@ mariadb: # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: - repository: booklore/booklore + repository: ghcr.io/the-booklore/booklore # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. diff --git a/example-docker/docker-compose.yml b/example-docker/docker-compose.yml index 26302777a..ba2d8e872 100644 --- a/example-docker/docker-compose.yml +++ b/example-docker/docker-compose.yml @@ -1,9 +1,6 @@ services: booklore: - # Official Docker Hub image: - image: booklore/booklore:latest - # Or the GHCR image: - # image: ghcr.io/booklore-app/booklore:latest + image: ghcr.io/the-booklore/booklore:latest container_name: booklore environment: - USER_ID=1000 # Modify this if the volume's ownership is not root @@ -51,4 +48,4 @@ services: test: [ "CMD", "mariadb-admin", "ping", "-h", "localhost" ] interval: 5s timeout: 5s - retries: 10 \ No newline at end of file + retries: 10 diff --git a/example-podman/README.md b/example-podman/README.md index 622b53396..3b5405d2e 100644 --- a/example-podman/README.md +++ b/example-podman/README.md @@ -10,7 +10,7 @@ # Setup ```bash echo -n "YOUR PASSWORD" | podman secret create booklore_db_pass - ``` -4. (Optional) `podman pull ghcr.io/booklore-app/booklore:latest` to pre-pull the image +4. (Optional) `podman pull ghcr.io/the-booklore/booklore:latest` to pre-pull the image * If you have a slow connection, this is recommended because systemd will time out if the image pull takes too long. 5. Run `systemctl --user daemon-reload` to pick up the new Quadlet unit. 6. Start the pod with `systemctl --user start booklore-pod.service` diff --git a/example-podman/booklore.container b/example-podman/booklore.container index 471b26d82..72e0222d0 100644 --- a/example-podman/booklore.container +++ b/example-podman/booklore.container @@ -5,7 +5,7 @@ After=booklore-db.container [Container] ContainerName=booklore -Image=ghcr.io/booklore-app/booklore:latest +Image=ghcr.io/the-booklore/booklore:latest Pod=booklore.pod AutoUpdate=registry Pull=always diff --git a/scripts/docker-buildx-push.sh b/scripts/docker-buildx-push.sh index f757f8f81..e614d7c79 100644 --- a/scripts/docker-buildx-push.sh +++ b/scripts/docker-buildx-push.sh @@ -14,7 +14,10 @@ fi VERSION="$1" +IMAGE_REF="ghcr.io/the-booklore/booklore:$VERSION" + echo "Building Booklore App with multi-arch version: $VERSION" +echo "Target registry image: $IMAGE_REF" # Ensure Docker Buildx builder exists and is used docker buildx create --use --name multiarch-builder || true @@ -22,8 +25,8 @@ docker buildx create --use --name multiarch-builder || true # Build and push multi-arch Docker image docker buildx build \ --platform linux/amd64,linux/arm64 \ - -t retr0spect101/booklore:"$VERSION" \ + -t "$IMAGE_REF" \ --push \ . -echo "Multi-arch Docker image retr0spect101/booklore:$VERSION pushed successfully!" \ No newline at end of file +echo "Multi-arch Docker image $IMAGE_REF pushed successfully!" diff --git a/scripts/notify-discord.sh b/scripts/notify-discord.sh index ebe6ef994..55e8a60d3 100644 --- a/scripts/notify-discord.sh +++ b/scripts/notify-discord.sh @@ -29,8 +29,7 @@ release_name=$(jq -r '.name' <<< "$release_json") release_body=$(jq -r '.body' <<< "$release_json") release_url=$(jq -r '.url' <<< "$release_json") -dockerhub_image="https://hub.docker.com/r/booklore/booklore/tags/$NEW_TAG" -ghcr_image="https://github.com/booklore-app/booklore/pkgs/container/booklore/$NEW_TAG" +ghcr_image="https://github.com/the-booklore/booklore/pkgs/container/booklore/$NEW_TAG" # Clean up body for Discord clean_body=$(echo "$release_body" | tr -d '\r') @@ -44,7 +43,6 @@ payload=$(jq -n \ --arg title "New Release: $release_name" \ --arg url "$release_url" \ --arg desc "$clean_body" \ - --arg hub "[View image]($dockerhub_image)" \ --arg gh "[View image]($ghcr_image)" \ '{ content: null, @@ -54,8 +52,7 @@ payload=$(jq -n \ description: $desc, color: 3066993, fields: [ - { name: "Docker Hub", value: $hub, inline: true }, - { name: "GHCR", value: $gh, inline: true } + { name: "GHCR", value: $gh, inline: true } ] }] }') @@ -70,4 +67,4 @@ echo "Sending notification to Discord..." curl -i -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" \ || echo "โš ๏ธ Request completed with an error; check the above HTTP response" -echo "Notification sent!" \ No newline at end of file +echo "Notification sent!" diff --git a/scripts/weblate-setup.sh b/scripts/weblate-setup.sh index f6554efb7..d6f89cf85 100755 --- a/scripts/weblate-setup.sh +++ b/scripts/weblate-setup.sh @@ -7,7 +7,7 @@ # # Prerequisites: # 1. Get your API token from https://hosted.weblate.org/accounts/profile/#api -# 2. Ensure your GitHub repo (booklore-app/booklore) is public (required for Libre plan) +# 2. Ensure your GitHub repo (the-booklore/booklore) is public (required for Libre plan) # # Usage: # WEBLATE_TOKEN=your-api-token ./scripts/weblate-setup.sh @@ -22,9 +22,9 @@ TOKEN="${WEBLATE_TOKEN:?Set WEBLATE_TOKEN to your Weblate API token}" PROJECT_NAME="BookLore" PROJECT_SLUG="booklore" -PROJECT_WEB="https://github.com/booklore-app/booklore" +PROJECT_WEB="https://github.com/the-booklore/booklore" -REPO_URL="https://github.com/booklore-app/booklore.git" +REPO_URL="https://github.com/the-booklore/booklore.git" REPO_BRANCH="develop" FILE_BASE="booklore-ui/src/i18n" From a8cdb8c63ae1c2031a60d65eb5e5276ec8f85f98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:01:37 -0600 Subject: [PATCH 2/5] chore(deps): bump release-drafter/release-drafter from 6.2.0 to 7.2.0 (#3363) Bumps [release-drafter/release-drafter](https://github.com/release-drafter/release-drafter) from 6.2.0 to 7.2.0. - [Release notes](https://github.com/release-drafter/release-drafter/releases) - [Commits](https://github.com/release-drafter/release-drafter/compare/6db134d15f3909ccc9eefd369f02bd1e9cffdf97...5de93583980a40bd78603b6dfdcda5b4df377b32) --- updated-dependencies: - dependency-name: release-drafter/release-drafter dependency-version: 7.2.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/master-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index ef2399f51..a104cff43 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -313,7 +313,7 @@ jobs: type=registry,ref=ghcr.io/the-booklore/booklore:buildcache,mode=max - name: Update GitHub Release Draft - uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6 + uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v6 with: tag: ${{ env.new_tag }} name: "Release ${{ env.new_tag }}" From cd079b6cff5e954f4de5da8aea146281073220ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 09:01:51 -0600 Subject: [PATCH 3/5] chore(deps): bump docker/login-action from 3.7.0 to 4.1.0 (#3362) Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.1.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/c94ce9fb468520275223c153574b00df6fe4bcc9...4907a6ddec9925e35a0a9e82d7399ccc52663121) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/develop-pipeline.yml | 2 +- .github/workflows/master-pipeline.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/develop-pipeline.yml b/.github/workflows/develop-pipeline.yml index f4b684971..7f988d55d 100644 --- a/.github/workflows/develop-pipeline.yml +++ b/.github/workflows/develop-pipeline.yml @@ -232,7 +232,7 @@ jobs: # ---------------------------------------- - name: Authenticate to GitHub Container Registry if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/master-pipeline.yml b/.github/workflows/master-pipeline.yml index a104cff43..00a14bb82 100644 --- a/.github/workflows/master-pipeline.yml +++ b/.github/workflows/master-pipeline.yml @@ -162,7 +162,7 @@ jobs: fetch-depth: 0 - name: Authenticate to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} From 385583d5d56c7a34c58bae40442b800c62ac2640 Mon Sep 17 00:00:00 2001 From: acx10 <8075870+acx10@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:20:05 -0600 Subject: [PATCH 4/5] Remove unused app endpoints (#3378) Co-authored-by: acx10 --- .../app/controller/AppAuthorController.java | 37 - .../app/controller/AppBookController.java | 112 - .../app/controller/AppFilterController.java | 26 - .../app/controller/AppLibraryController.java | 53 - .../app/controller/AppNotebookController.java | 60 - .../app/controller/AppSeriesController.java | 50 - .../app/controller/AppShelfController.java | 91 - .../app/controller/AppUserController.java | 46 - .../org/booklore/app/dto/AppAuthorDetail.java | 21 - .../booklore/app/dto/AppAuthorSummary.java | 20 - .../org/booklore/app/dto/AppBookDetail.java | 110 - .../org/booklore/app/dto/AppBookFile.java | 28 - .../org/booklore/app/dto/AppBookSummary.java | 34 - .../booklore/app/dto/AppFilterOptions.java | 40 - .../booklore/app/dto/AppLibrarySummary.java | 33 - .../app/dto/AppMagicShelfSummary.java | 20 - .../app/dto/AppNotebookBookSummary.java | 23 - .../booklore/app/dto/AppNotebookEntry.java | 27 - .../app/dto/AppNotebookUpdateRequest.java | 20 - .../org/booklore/app/dto/AppPageResponse.java | 37 - .../booklore/app/dto/AppSeriesSummary.java | 22 - .../org/booklore/app/dto/AppShelfSummary.java | 20 - .../org/booklore/app/dto/AppUserInfo.java | 18 - .../org/booklore/app/dto/SeriesCoverBook.java | 18 - .../booklore/app/dto/UpdateRatingRequest.java | 12 - .../booklore/app/dto/UpdateStatusRequest.java | 11 - .../booklore/app/mapper/AppBookMapper.java | 345 --- .../app/service/AppAuthorService.java | 220 -- .../booklore/app/service/AppBookService.java | 698 ----- .../app/service/AppNotebookService.java | 197 -- .../app/service/AppSeriesService.java | 390 --- .../specification/AppBookSpecification.java | 253 -- .../controller/AppFilterControllerTest.java | 89 - .../controller/AppSeriesControllerTest.java | 158 -- .../app/service/AppAuthorServiceTest.java | 377 --- .../AppBookServiceFilterOptionsTest.java | 260 -- .../app/service/AppSeriesServiceTest.java | 488 ---- booklore-ui/package-lock.json | 2261 ++++++++++------- 38 files changed, 1321 insertions(+), 5404 deletions(-) delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppAuthorController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppBookController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppFilterController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppLibraryController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppNotebookController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppSeriesController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppShelfController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/controller/AppUserController.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppAuthorDetail.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppAuthorSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppBookDetail.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppBookFile.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppBookSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppFilterOptions.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppLibrarySummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppMagicShelfSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppNotebookBookSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppNotebookEntry.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppNotebookUpdateRequest.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppPageResponse.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppSeriesSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppShelfSummary.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/AppUserInfo.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/SeriesCoverBook.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/UpdateRatingRequest.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/dto/UpdateStatusRequest.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/mapper/AppBookMapper.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/service/AppAuthorService.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/service/AppBookService.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/service/AppNotebookService.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java delete mode 100644 booklore-api/src/main/java/org/booklore/app/specification/AppBookSpecification.java delete mode 100644 booklore-api/src/test/java/org/booklore/app/controller/AppFilterControllerTest.java delete mode 100644 booklore-api/src/test/java/org/booklore/app/controller/AppSeriesControllerTest.java delete mode 100644 booklore-api/src/test/java/org/booklore/app/service/AppAuthorServiceTest.java delete mode 100644 booklore-api/src/test/java/org/booklore/app/service/AppBookServiceFilterOptionsTest.java delete mode 100644 booklore-api/src/test/java/org/booklore/app/service/AppSeriesServiceTest.java diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppAuthorController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppAuthorController.java deleted file mode 100644 index 07523e5bc..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppAuthorController.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.AppAuthorDetail; -import org.booklore.app.dto.AppAuthorSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.service.AppAuthorService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/authors") -public class AppAuthorController { - - private final AppAuthorService mobileAuthorService; - - @GetMapping - public ResponseEntity> getAuthors( - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "30") Integer size, - @RequestParam(required = false, defaultValue = "name") String sort, - @RequestParam(required = false, defaultValue = "asc") String dir, - @RequestParam(required = false) Long libraryId, - @RequestParam(required = false) String search, - @RequestParam(required = false) Boolean hasPhoto) { - - return ResponseEntity.ok(mobileAuthorService.getAuthors(page, size, sort, dir, libraryId, search, hasPhoto)); - } - - @GetMapping("/{authorId}") - public ResponseEntity getAuthorDetail( - @PathVariable Long authorId) { - - return ResponseEntity.ok(mobileAuthorService.getAuthorDetail(authorId)); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppBookController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppBookController.java deleted file mode 100644 index 0b3c04061..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppBookController.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.*; -import org.booklore.app.service.AppBookService; -import org.booklore.model.enums.BookFileType; -import org.booklore.model.enums.ReadStatus; -import jakarta.validation.Valid; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/books") -public class AppBookController { - - private final AppBookService mobileBookService; - - @GetMapping - public ResponseEntity> getBooks( - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "50") Integer size, - @RequestParam(required = false, defaultValue = "addedOn") String sort, - @RequestParam(required = false, defaultValue = "desc") String dir, - @RequestParam(required = false) Long libraryId, - @RequestParam(required = false) Long shelfId, - @RequestParam(required = false) ReadStatus status, - @RequestParam(required = false) String search, - @RequestParam(required = false) BookFileType fileType, - @RequestParam(required = false) Integer minRating, - @RequestParam(required = false) Integer maxRating, - @RequestParam(required = false) String authors, - @RequestParam(required = false) String language) { - - return ResponseEntity.ok(mobileBookService.getBooks( - page, size, sort, dir, libraryId, shelfId, status, search, - fileType, minRating, maxRating, authors, language)); - } - - @GetMapping("/{bookId}") - public ResponseEntity getBookDetail( - @PathVariable Long bookId) { - - return ResponseEntity.ok(mobileBookService.getBookDetail(bookId)); - } - - @GetMapping("/search") - public ResponseEntity> searchBooks( - @RequestParam String q, - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size) { - - return ResponseEntity.ok(mobileBookService.searchBooks(q, page, size)); - } - - @GetMapping("/continue-reading") - public ResponseEntity> getContinueReading( - @RequestParam(required = false, defaultValue = "10") Integer limit) { - - return ResponseEntity.ok(mobileBookService.getContinueReading(limit)); - } - - @GetMapping("/continue-listening") - public ResponseEntity> getContinueListening( - @RequestParam(required = false, defaultValue = "10") Integer limit) { - - return ResponseEntity.ok(mobileBookService.getContinueListening(limit)); - } - - @GetMapping("/recently-added") - public ResponseEntity> getRecentlyAdded( - @RequestParam(required = false, defaultValue = "10") Integer limit) { - - return ResponseEntity.ok(mobileBookService.getRecentlyAdded(limit)); - } - - @GetMapping("/recently-scanned") - public ResponseEntity> getRecentlyScanned( - @RequestParam(required = false, defaultValue = "10") Integer limit) { - - return ResponseEntity.ok(mobileBookService.getRecentlyScanned(limit)); - } - - @PutMapping("/{bookId}/status") - public ResponseEntity updateStatus( - @PathVariable Long bookId, - @Valid @RequestBody UpdateStatusRequest request) { - - mobileBookService.updateReadStatus(bookId, request.getStatus()); - return ResponseEntity.ok().build(); - } - - @PutMapping("/{bookId}/rating") - public ResponseEntity updateRating( - @PathVariable Long bookId, - @Valid @RequestBody UpdateRatingRequest request) { - - mobileBookService.updatePersonalRating(bookId, request.getRating()); - return ResponseEntity.ok().build(); - } - - @GetMapping("/random") - public ResponseEntity> getRandomBooks( - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size, - @RequestParam(required = false) Long libraryId) { - - return ResponseEntity.ok(mobileBookService.getRandomBooks(page, size, libraryId)); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppFilterController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppFilterController.java deleted file mode 100644 index f8a231a1f..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppFilterController.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.AppFilterOptions; -import org.booklore.app.service.AppBookService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app") -public class AppFilterController { - - private final AppBookService mobileBookService; - - @GetMapping("/filter-options") - public ResponseEntity getFilterOptions( - @RequestParam(required = false) Long libraryId, - @RequestParam(required = false) Long shelfId, - @RequestParam(required = false) Long magicShelfId) { - return ResponseEntity.ok(mobileBookService.getFilterOptions(libraryId, shelfId, magicShelfId)); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppLibraryController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppLibraryController.java deleted file mode 100644 index 4a24cab62..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppLibraryController.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.app.dto.AppLibrarySummary; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.LibraryEntity; -import org.booklore.repository.BookRepository; -import org.booklore.repository.LibraryRepository; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.stream.Collectors; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/libraries") -public class AppLibraryController { - - private final AuthenticationService authenticationService; - private final LibraryRepository libraryRepository; - private final BookRepository bookRepository; - private final AppBookMapper mobileBookMapper; - - @GetMapping - public ResponseEntity> getLibraries() { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - - List libraries; - if (user.getPermissions().isAdmin()) { - libraries = libraryRepository.findAll(); - } else { - List libraryIds = user.getAssignedLibraries() != null - ? user.getAssignedLibraries().stream().map(Library::getId).collect(Collectors.toList()) - : List.of(); - libraries = libraryRepository.findByIdIn(libraryIds); - } - - List summaries = libraries.stream() - .map(library -> { - long bookCount = bookRepository.countByLibraryId(library.getId()); - return mobileBookMapper.toLibrarySummary(library, bookCount); - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(summaries); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppNotebookController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppNotebookController.java deleted file mode 100644 index 6c8887780..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppNotebookController.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.AppNotebookBookSummary; -import org.booklore.app.dto.AppNotebookEntry; -import org.booklore.app.dto.AppNotebookUpdateRequest; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.service.AppNotebookService; -import jakarta.validation.Valid; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Set; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/notebook") -public class AppNotebookController { - - private final AppNotebookService mobileNotebookService; - - @GetMapping("/books") - public ResponseEntity> getBooksWithAnnotations( - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size, - @RequestParam(required = false) String search) { - - return ResponseEntity.ok(mobileNotebookService.getBooksWithAnnotations(page, size, search)); - } - - @GetMapping("/books/{bookId}/entries") - public ResponseEntity> getEntriesForBook( - @PathVariable Long bookId, - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size, - @RequestParam(required = false) Set types, - @RequestParam(required = false) String search, - @RequestParam(required = false, defaultValue = "date_desc") String sort) { - - return ResponseEntity.ok(mobileNotebookService.getEntriesForBook(bookId, page, size, types, search, sort)); - } - - @PutMapping("/entries/{entryId}") - public ResponseEntity updateEntry( - @PathVariable Long entryId, - @RequestParam String type, - @Valid @RequestBody AppNotebookUpdateRequest request) { - - return ResponseEntity.ok(mobileNotebookService.updateEntry(entryId, type, request)); - } - - @DeleteMapping("/entries/{entryId}") - public ResponseEntity deleteEntry( - @PathVariable Long entryId, - @RequestParam String type) { - - mobileNotebookService.deleteEntry(entryId, type); - return ResponseEntity.noContent().build(); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppSeriesController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppSeriesController.java deleted file mode 100644 index 6b47a9c56..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppSeriesController.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.booklore.app.controller; - -import lombok.AllArgsConstructor; -import org.booklore.app.dto.AppBookSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.dto.AppSeriesSummary; -import org.booklore.app.service.AppSeriesService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/series") -public class AppSeriesController { - - private final AppSeriesService mobileSeriesService; - - @GetMapping - public ResponseEntity> getSeries( - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size, - @RequestParam(required = false, defaultValue = "recentlyAdded") String sort, - @RequestParam(required = false, defaultValue = "desc") String dir, - @RequestParam(required = false) Long libraryId, - @RequestParam(required = false) String search, - @RequestParam(required = false) String status) { - - boolean inProgressOnly = "in-progress".equalsIgnoreCase(status); - - AppPageResponse response = mobileSeriesService.getSeries( - page, size, sort, dir, libraryId, search, inProgressOnly); - - return ResponseEntity.ok(response); - } - - @GetMapping("/{seriesName}/books") - public ResponseEntity> getSeriesBooks( - @PathVariable String seriesName, - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size, - @RequestParam(required = false, defaultValue = "seriesNumber") String sort, - @RequestParam(required = false, defaultValue = "asc") String dir, - @RequestParam(required = false) Long libraryId) { - - AppPageResponse response = mobileSeriesService.getSeriesBooks( - seriesName, page, size, sort, dir, libraryId); - - return ResponseEntity.ok(response); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppShelfController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppShelfController.java deleted file mode 100644 index b12ac6d5b..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppShelfController.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.app.dto.AppBookSummary; -import org.booklore.app.dto.AppMagicShelfSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.dto.AppShelfSummary; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.app.service.AppBookService; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.entity.MagicShelfEntity; -import org.booklore.model.entity.ShelfEntity; -import org.booklore.repository.MagicShelfRepository; -import org.booklore.repository.ShelfRepository; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/shelves") -public class AppShelfController { - - private final AuthenticationService authenticationService; - private final ShelfRepository shelfRepository; - private final MagicShelfRepository magicShelfRepository; - private final AppBookMapper mobileBookMapper; - private final AppBookService mobileBookService; - - @GetMapping - public ResponseEntity> getShelves() { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - - List shelves = shelfRepository.findByUserIdOrPublicShelfTrue(userId); - - List summaries = shelves.stream() - .map(mobileBookMapper::toShelfSummaryFromEntity) - .collect(Collectors.toList()); - - return ResponseEntity.ok(summaries); - } - - @GetMapping("/magic") - public ResponseEntity> getMagicShelves() { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - - // Get user's own magic shelves - List userShelves = magicShelfRepository.findAllByUserId(userId); - - // Get public magic shelves - List publicShelves = magicShelfRepository.findAllByIsPublicIsTrue(); - - // Combine and deduplicate (user's shelves that are also public shouldn't appear twice) - Set seenIds = new HashSet<>(); - List allShelves = new ArrayList<>(); - - for (MagicShelfEntity shelf : userShelves) { - if (seenIds.add(shelf.getId())) { - allShelves.add(shelf); - } - } - for (MagicShelfEntity shelf : publicShelves) { - if (seenIds.add(shelf.getId())) { - allShelves.add(shelf); - } - } - - List summaries = allShelves.stream() - .map(mobileBookMapper::toMagicShelfSummary) - .collect(Collectors.toList()); - - return ResponseEntity.ok(summaries); - } - - @GetMapping("/magic/{magicShelfId}/books") - public ResponseEntity> getBooksByMagicShelf( - @PathVariable Long magicShelfId, - @RequestParam(required = false, defaultValue = "0") Integer page, - @RequestParam(required = false, defaultValue = "20") Integer size) { - - return ResponseEntity.ok(mobileBookService.getBooksByMagicShelf(magicShelfId, page, size)); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/controller/AppUserController.java b/booklore-api/src/main/java/org/booklore/app/controller/AppUserController.java deleted file mode 100644 index fc54d3f06..000000000 --- a/booklore-api/src/main/java/org/booklore/app/controller/AppUserController.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.app.dto.AppUserInfo; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.service.appsettings.AppSettingService; -import lombok.AllArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@AllArgsConstructor -@RestController -@RequestMapping("/api/v1/app/users") -public class AppUserController { - - private final AuthenticationService authenticationService; - private final AppSettingService appSettingService; - - @GetMapping("/me") - public ResponseEntity getCurrentUser() { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - BookLoreUser.UserPermissions perms = user.getPermissions(); - - int maxUploadSizeMb = 100; // default - try { - Integer configured = appSettingService.getAppSettings().getMaxFileUploadSizeInMb(); - if (configured != null) { - maxUploadSizeMb = configured; - } - } catch (Exception ignored) { - // fall back to default - } - - AppUserInfo info = AppUserInfo.builder() - .isAdmin(perms.isAdmin()) - .canUpload(perms.isCanUpload()) - .canDownload(perms.isCanDownload()) - .canAccessBookdrop(perms.isCanAccessBookdrop()) - .maxFileUploadSizeMb(maxUploadSizeMb) - .build(); - - return ResponseEntity.ok(info); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorDetail.java b/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorDetail.java deleted file mode 100644 index 97770734b..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorDetail.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppAuthorDetail { - private Long id; - private String name; - private String description; - private String asin; - private int bookCount; - private boolean hasPhoto; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorSummary.java deleted file mode 100644 index de999749a..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppAuthorSummary.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppAuthorSummary { - private Long id; - private String name; - private String asin; - private int bookCount; - private boolean hasPhoto; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppBookDetail.java b/booklore-api/src/main/java/org/booklore/app/dto/AppBookDetail.java deleted file mode 100644 index 935cf4ef8..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppBookDetail.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.time.LocalDate; -import java.util.List; -import java.util.Set; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppBookDetail { - private Long id; - private String title; - private List authors; - private String thumbnailUrl; - private String readStatus; - private Integer personalRating; - private String seriesName; - private Float seriesNumber; - private Long libraryId; - private Instant addedOn; - private Instant lastReadTime; - - private String subtitle; - private String description; - private Set categories; - private String publisher; - private LocalDate publishedDate; - private Integer pageCount; - private String isbn13; - private String language; - private Double goodreadsRating; - private Integer goodreadsReviewCount; - private String libraryName; - private List shelves; - private Float readProgress; - private String primaryFileType; - private List fileTypes; - private List files; - private Instant coverUpdatedOn; - private Instant audiobookCoverUpdatedOn; - private Boolean isPhysical; - - private EpubProgress epubProgress; - private PdfProgress pdfProgress; - private CbxProgress cbxProgress; - private AudiobookProgress audiobookProgress; - private KoreaderProgress koreaderProgress; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class EpubProgress { - private String cfi; - private String href; - private Float percentage; - private Instant updatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class PdfProgress { - private Integer page; - private Float percentage; - private Instant updatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class CbxProgress { - private Integer page; - private Float percentage; - private Instant updatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class AudiobookProgress { - private Long positionMs; - private Integer trackIndex; - private Float percentage; - private Instant updatedAt; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class KoreaderProgress { - private Float percentage; - private String device; - private String deviceId; - private Instant lastSyncTime; - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppBookFile.java b/booklore-api/src/main/java/org/booklore/app/dto/AppBookFile.java deleted file mode 100644 index bf8f1cf04..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppBookFile.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppBookFile { - private Long id; - private Long bookId; - private String fileName; - private boolean isBook; - private boolean folderBased; - private String bookType; - private String archiveType; - private Long fileSizeKb; - private String extension; - private Instant addedOn; - private boolean isPrimary; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppBookSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppBookSummary.java deleted file mode 100644 index c25a3d3ea..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppBookSummary.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppBookSummary { - private Long id; - private String title; - private List authors; - private String thumbnailUrl; - private String readStatus; - private Integer personalRating; - private String seriesName; - private Float seriesNumber; - private Long libraryId; - private Instant addedOn; - private Instant lastReadTime; - private Float readProgress; - private String primaryFileType; - private Instant coverUpdatedOn; - private Instant audiobookCoverUpdatedOn; - private Boolean isPhysical; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppFilterOptions.java b/booklore-api/src/main/java/org/booklore/app/dto/AppFilterOptions.java deleted file mode 100644 index 60860d7e1..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppFilterOptions.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppFilterOptions { - private List authors; - private List languages; - private List readStatuses; - private List fileTypes; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class AuthorOption { - private String name; - private long count; - } - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class LanguageOption { - private String code; - private String label; - private long count; - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppLibrarySummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppLibrarySummary.java deleted file mode 100644 index 5ed216035..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppLibrarySummary.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.booklore.model.enums.BookFileType; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppLibrarySummary { - private Long id; - private String name; - private String icon; - private long bookCount; - private List allowedFormats; - private List paths; - - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class PathSummary { - private Long id; - private String path; - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppMagicShelfSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppMagicShelfSummary.java deleted file mode 100644 index ae276f4be..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppMagicShelfSummary.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppMagicShelfSummary { - private Long id; - private String name; - private String icon; - private String iconType; - private boolean publicShelf; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookBookSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookBookSummary.java deleted file mode 100644 index 00c94407e..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookBookSummary.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppNotebookBookSummary { - private Long bookId; - private String bookTitle; - private int noteCount; - private List authors; - private Instant coverUpdatedOn; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookEntry.java b/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookEntry.java deleted file mode 100644 index e28a1acda..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookEntry.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppNotebookEntry { - private Long id; - private String type; - private Long bookId; - private String text; - private String note; - private String color; - private String style; - private String chapterTitle; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookUpdateRequest.java b/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookUpdateRequest.java deleted file mode 100644 index 69d140000..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppNotebookUpdateRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.booklore.app.dto; - -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AppNotebookUpdateRequest { - @Size(max = 5000) - private String note; - - @Pattern(regexp = "^#[0-9A-Fa-f]{6}$") - private String color; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppPageResponse.java b/booklore-api/src/main/java/org/booklore/app/dto/AppPageResponse.java deleted file mode 100644 index 78930ad9c..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppPageResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppPageResponse { - private List content; - private int page; - private int size; - private long totalElements; - private int totalPages; - private boolean hasNext; - private boolean hasPrevious; - - public static AppPageResponse of(List content, int page, int size, long totalElements) { - int totalPages = size > 0 ? (int) Math.ceil((double) totalElements / size) : 0; - return AppPageResponse.builder() - .content(content) - .page(page) - .size(size) - .totalElements(totalElements) - .totalPages(totalPages) - .hasNext(page < totalPages - 1) - .hasPrevious(page > 0) - .build(); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppSeriesSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppSeriesSummary.java deleted file mode 100644 index 5a47c63e5..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppSeriesSummary.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.*; - -import java.time.Instant; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppSeriesSummary { - private String seriesName; - private int bookCount; - private Integer seriesTotal; - private List authors; - private int booksRead; - private Instant latestAddedOn; - private List coverBooks; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppShelfSummary.java b/booklore-api/src/main/java/org/booklore/app/dto/AppShelfSummary.java deleted file mode 100644 index 2b332ea29..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppShelfSummary.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AppShelfSummary { - private Long id; - private String name; - private String icon; - private int bookCount; - private boolean publicShelf; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/AppUserInfo.java b/booklore-api/src/main/java/org/booklore/app/dto/AppUserInfo.java deleted file mode 100644 index 59a3297e5..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/AppUserInfo.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.booklore.app.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AppUserInfo { - private boolean isAdmin; - private boolean canUpload; - private boolean canDownload; - private boolean canAccessBookdrop; - private int maxFileUploadSizeMb; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/SeriesCoverBook.java b/booklore-api/src/main/java/org/booklore/app/dto/SeriesCoverBook.java deleted file mode 100644 index 687233f40..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/SeriesCoverBook.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.booklore.app.dto; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.*; - -import java.time.Instant; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SeriesCoverBook { - private Long bookId; - private Instant coverUpdatedOn; - private Float seriesNumber; - private String primaryFileType; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/UpdateRatingRequest.java b/booklore-api/src/main/java/org/booklore/app/dto/UpdateRatingRequest.java deleted file mode 100644 index 7806f86e6..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/UpdateRatingRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.booklore.app.dto; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import lombok.Data; - -@Data -public class UpdateRatingRequest { - @Min(value = 1, message = "Rating must be at least 1") - @Max(value = 5, message = "Rating must be at most 5") - private Integer rating; -} diff --git a/booklore-api/src/main/java/org/booklore/app/dto/UpdateStatusRequest.java b/booklore-api/src/main/java/org/booklore/app/dto/UpdateStatusRequest.java deleted file mode 100644 index bbdcb82a3..000000000 --- a/booklore-api/src/main/java/org/booklore/app/dto/UpdateStatusRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.booklore.app.dto; - -import org.booklore.model.enums.ReadStatus; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Data -public class UpdateStatusRequest { - @NotNull(message = "Status is required") - private ReadStatus status; -} diff --git a/booklore-api/src/main/java/org/booklore/app/mapper/AppBookMapper.java b/booklore-api/src/main/java/org/booklore/app/mapper/AppBookMapper.java deleted file mode 100644 index 96fb6c704..000000000 --- a/booklore-api/src/main/java/org/booklore/app/mapper/AppBookMapper.java +++ /dev/null @@ -1,345 +0,0 @@ -package org.booklore.app.mapper; - -import org.booklore.app.dto.AppBookDetail; -import org.booklore.app.dto.AppBookFile; -import org.booklore.app.dto.AppBookSummary; -import org.booklore.app.dto.AppLibrarySummary; -import org.booklore.app.dto.AppMagicShelfSummary; -import org.booklore.app.dto.AppShelfSummary; -import org.booklore.model.entity.*; -import org.booklore.model.enums.BookFileType; -import org.mapstruct.*; - -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) -public interface AppBookMapper { - - @Mapping(target = "id", source = "book.id") - @Mapping(target = "title", source = "book.metadata.title") - @Mapping(target = "authors", source = "book.metadata.authors", qualifiedByName = "mapAuthors") - @Mapping(target = "thumbnailUrl", source = "book", qualifiedByName = "mapThumbnailUrl") - @Mapping(target = "readStatus", source = "progress.readStatus") - @Mapping(target = "personalRating", source = "progress.personalRating") - @Mapping(target = "seriesName", source = "book.metadata.seriesName") - @Mapping(target = "seriesNumber", source = "book.metadata.seriesNumber") - @Mapping(target = "libraryId", source = "book.library.id") - @Mapping(target = "addedOn", source = "book.addedOn") - @Mapping(target = "lastReadTime", source = "progress.lastReadTime") - @Mapping(target = "readProgress", source = "progress", qualifiedByName = "mapReadProgress") - @Mapping(target = "primaryFileType", source = "book", qualifiedByName = "mapPrimaryFileType") - @Mapping(target = "coverUpdatedOn", source = "book.metadata.coverUpdatedOn") - @Mapping(target = "audiobookCoverUpdatedOn", source = "book.metadata.audiobookCoverUpdatedOn") - @Mapping(target = "isPhysical", source = "book.isPhysical") - AppBookSummary toSummary(BookEntity book, UserBookProgressEntity progress); - - @Mapping(target = "id", source = "book.id") - @Mapping(target = "title", source = "book.metadata.title") - @Mapping(target = "authors", source = "book.metadata.authors", qualifiedByName = "mapAuthors") - @Mapping(target = "thumbnailUrl", source = "book", qualifiedByName = "mapThumbnailUrl") - @Mapping(target = "readStatus", source = "progress.readStatus") - @Mapping(target = "personalRating", source = "progress.personalRating") - @Mapping(target = "seriesName", source = "book.metadata.seriesName") - @Mapping(target = "seriesNumber", source = "book.metadata.seriesNumber") - @Mapping(target = "libraryId", source = "book.library.id") - @Mapping(target = "addedOn", source = "book.addedOn") - @Mapping(target = "lastReadTime", source = "progress.lastReadTime") - @Mapping(target = "subtitle", source = "book.metadata.subtitle") - @Mapping(target = "description", source = "book.metadata.description") - @Mapping(target = "categories", source = "book.metadata.categories", qualifiedByName = "mapCategories") - @Mapping(target = "publisher", source = "book.metadata.publisher") - @Mapping(target = "publishedDate", source = "book.metadata.publishedDate") - @Mapping(target = "pageCount", source = "book.metadata.pageCount") - @Mapping(target = "isbn13", source = "book.metadata.isbn13") - @Mapping(target = "language", source = "book.metadata.language") - @Mapping(target = "goodreadsRating", source = "book.metadata.goodreadsRating") - @Mapping(target = "goodreadsReviewCount", source = "book.metadata.goodreadsReviewCount") - @Mapping(target = "libraryName", source = "book.library.name") - @Mapping(target = "shelves", source = "book.shelves", qualifiedByName = "mapShelves") - @Mapping(target = "readProgress", source = "progress", qualifiedByName = "mapReadProgress") - @Mapping(target = "primaryFileType", source = "book", qualifiedByName = "mapPrimaryFileType") - @Mapping(target = "coverUpdatedOn", source = "book.metadata.coverUpdatedOn") - @Mapping(target = "audiobookCoverUpdatedOn", source = "book.metadata.audiobookCoverUpdatedOn") - @Mapping(target = "isPhysical", source = "book.isPhysical") - @Mapping(target = "fileTypes", source = "book", qualifiedByName = "mapFileTypes") - @Mapping(target = "files", source = "book", qualifiedByName = "mapFiles") - @Mapping(target = "epubProgress", source = "progress", qualifiedByName = "mapEpubProgress") - @Mapping(target = "pdfProgress", source = "progress", qualifiedByName = "mapPdfProgress") - @Mapping(target = "cbxProgress", source = "progress", qualifiedByName = "mapCbxProgress") - @Mapping(target = "audiobookProgress", source = "fileProgress", qualifiedByName = "mapAudiobookProgress") - @Mapping(target = "koreaderProgress", source = "progress", qualifiedByName = "mapKoreaderProgress") - AppBookDetail toDetail(BookEntity book, UserBookProgressEntity progress, UserBookFileProgressEntity fileProgress); - - @Named("mapAuthors") - default List mapAuthors(List authors) { - if (authors == null || authors.isEmpty()) { - return Collections.emptyList(); - } - return authors.stream() - .map(AuthorEntity::getName) - .toList(); - } - - @Named("mapCategories") - default Set mapCategories(Set categories) { - if (categories == null || categories.isEmpty()) { - return Collections.emptySet(); - } - return categories.stream() - .map(CategoryEntity::getName) - .collect(Collectors.toSet()); - } - - @Named("mapThumbnailUrl") - default String mapThumbnailUrl(BookEntity book) { - if (book == null || book.getId() == null) { - return null; - } - return "/api/books/" + book.getId() + "/cover"; - } - - @Named("mapShelves") - default List mapShelves(Set shelves) { - if (shelves == null || shelves.isEmpty()) { - return Collections.emptyList(); - } - return shelves.stream() - .map(this::toShelfSummary) - .collect(Collectors.toList()); - } - - default AppShelfSummary toShelfSummary(ShelfEntity shelf) { - if (shelf == null) { - return null; - } - return AppShelfSummary.builder() - .id(shelf.getId()) - .name(shelf.getName()) - .icon(shelf.getIcon()) - .bookCount(shelf.getBookEntities() != null ? shelf.getBookEntities().size() : 0) - .publicShelf(shelf.isPublic()) - .build(); - } - - @Named("mapReadProgress") - default Float mapReadProgress(UserBookProgressEntity progress) { - if (progress == null) { - return null; - } - if (progress.getKoreaderProgressPercent() != null) { - return progress.getKoreaderProgressPercent(); - } - if (progress.getKoboProgressPercent() != null) { - return progress.getKoboProgressPercent(); - } - if (progress.getEpubProgressPercent() != null) { - return progress.getEpubProgressPercent(); - } - if (progress.getPdfProgressPercent() != null) { - return progress.getPdfProgressPercent(); - } - if (progress.getCbxProgressPercent() != null) { - return progress.getCbxProgressPercent(); - } - return null; - } - - @Named("mapEpubProgress") - default AppBookDetail.EpubProgress mapEpubProgress(UserBookProgressEntity progress) { - if (progress == null || progress.getEpubProgress() == null) { - return null; - } - return AppBookDetail.EpubProgress.builder() - .cfi(progress.getEpubProgress()) - .href(progress.getEpubProgressHref()) - .percentage(progress.getEpubProgressPercent()) - .updatedAt(progress.getLastReadTime()) - .build(); - } - - @Named("mapPdfProgress") - default AppBookDetail.PdfProgress mapPdfProgress(UserBookProgressEntity progress) { - if (progress == null || progress.getPdfProgress() == null) { - return null; - } - return AppBookDetail.PdfProgress.builder() - .page(progress.getPdfProgress()) - .percentage(progress.getPdfProgressPercent()) - .updatedAt(progress.getLastReadTime()) - .build(); - } - - @Named("mapCbxProgress") - default AppBookDetail.CbxProgress mapCbxProgress(UserBookProgressEntity progress) { - if (progress == null || progress.getCbxProgress() == null) { - return null; - } - return AppBookDetail.CbxProgress.builder() - .page(progress.getCbxProgress()) - .percentage(progress.getCbxProgressPercent()) - .updatedAt(progress.getLastReadTime()) - .build(); - } - - @Named("mapKoreaderProgress") - default AppBookDetail.KoreaderProgress mapKoreaderProgress(UserBookProgressEntity progress) { - if (progress == null || progress.getKoreaderProgressPercent() == null) { - return null; - } - return AppBookDetail.KoreaderProgress.builder() - .percentage(progress.getKoreaderProgressPercent()) - .device(progress.getKoreaderDevice()) - .deviceId(progress.getKoreaderDeviceId()) - .lastSyncTime(progress.getKoreaderLastSyncTime()) - .build(); - } - - @Named("mapAudiobookProgress") - default AppBookDetail.AudiobookProgress mapAudiobookProgress(UserBookFileProgressEntity fileProgress) { - if (fileProgress == null) return null; - if (fileProgress.getBookFile() == null || - fileProgress.getBookFile().getBookType() != BookFileType.AUDIOBOOK) { - return null; - } - - return AppBookDetail.AudiobookProgress.builder() - .positionMs(parseLongOrNull(fileProgress.getPositionData())) - .trackIndex(parseIntOrNull(fileProgress.getPositionHref())) - .percentage(fileProgress.getProgressPercent()) - .updatedAt(fileProgress.getLastReadTime()) - .build(); - } - - default Long parseLongOrNull(String value) { - if (value == null) return null; - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return null; - } - } - - default Integer parseIntOrNull(String value) { - if (value == null) return null; - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return null; - } - } - - @Named("mapPrimaryFileType") - default String mapPrimaryFileType(BookEntity book) { - if (book == null) { - return null; - } - BookFileEntity primaryFile = book.getPrimaryBookFile(); - if (primaryFile != null && primaryFile.getBookType() != null) { - return primaryFile.getBookType().name(); - } - return null; - } - - @Named("mapFileTypes") - default List mapFileTypes(BookEntity book) { - if (book == null || book.getBookFiles() == null || book.getBookFiles().isEmpty()) { - return Collections.emptyList(); - } - return book.getBookFiles().stream() - .filter(bf -> bf.getBookType() != null) - .map(bf -> bf.getBookType().name()) - .distinct() - .collect(Collectors.toList()); - } - - @Named("mapFiles") - default List mapFiles(BookEntity book) { - if (book == null || book.getBookFiles() == null || book.getBookFiles().isEmpty()) { - return Collections.emptyList(); - } - BookFileEntity primaryFile = book.getPrimaryBookFile(); - Long primaryId = primaryFile != null ? primaryFile.getId() : null; - - return book.getBookFiles().stream() - .filter(bf -> bf.getBookType() != null && bf.isBook()) - .map(bf -> { - String extension = null; - try { - String fileName = bf.getFileName(); - int lastDot = fileName.lastIndexOf('.'); - if (lastDot > 0) { - extension = fileName.substring(lastDot + 1); - } - } catch (Exception e) { - // Handle case where extension cannot be extracted - } - - return AppBookFile.builder() - .id(bf.getId()) - .bookId(bf.getBook() != null ? bf.getBook().getId() : null) - .fileName(bf.getFileName()) - .isBook(bf.isBook()) - .folderBased(bf.isFolderBased()) - .bookType(bf.getBookType().name()) - .archiveType(bf.getArchiveType() != null ? bf.getArchiveType().name() : null) - .fileSizeKb(bf.getFileSizeKb()) - .extension(extension) - .addedOn(bf.getAddedOn()) - .isPrimary(bf.getId().equals(primaryId)) - .build(); - }) - .collect(Collectors.toList()); - } - - default AppLibrarySummary toLibrarySummary(LibraryEntity library, long bookCount) { - if (library == null) { - return null; - } - List paths = Collections.emptyList(); - if (library.getLibraryPaths() != null && !library.getLibraryPaths().isEmpty()) { - paths = library.getLibraryPaths().stream() - .map(lp -> AppLibrarySummary.PathSummary.builder() - .id(lp.getId()) - .path(lp.getPath()) - .build()) - .collect(Collectors.toList()); - } - return AppLibrarySummary.builder() - .id(library.getId()) - .name(library.getName()) - .icon(library.getIcon()) - .bookCount(bookCount) - .allowedFormats(library.getAllowedFormats()) - .paths(paths) - .build(); - } - - default AppShelfSummary toShelfSummaryFromEntity(ShelfEntity shelf) { - if (shelf == null) { - return null; - } - return AppShelfSummary.builder() - .id(shelf.getId()) - .name(shelf.getName()) - .icon(shelf.getIcon()) - .bookCount(shelf.getBookEntities() != null ? shelf.getBookEntities().size() : 0) - .publicShelf(shelf.isPublic()) - .build(); - } - - default AppMagicShelfSummary toMagicShelfSummary(MagicShelfEntity magicShelf) { - if (magicShelf == null) { - return null; - } - return AppMagicShelfSummary.builder() - .id(magicShelf.getId()) - .name(magicShelf.getName()) - .icon(magicShelf.getIcon()) - .iconType(magicShelf.getIconType() != null ? magicShelf.getIconType().name() : null) - .publicShelf(magicShelf.isPublic()) - .build(); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/service/AppAuthorService.java b/booklore-api/src/main/java/org/booklore/app/service/AppAuthorService.java deleted file mode 100644 index 47e9d254e..000000000 --- a/booklore-api/src/main/java/org/booklore/app/service/AppAuthorService.java +++ /dev/null @@ -1,220 +0,0 @@ -package org.booklore.app.service; - -import lombok.AllArgsConstructor; -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.ApiError; -import org.booklore.app.dto.AppAuthorDetail; -import org.booklore.app.dto.AppAuthorSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.AuthorEntity; -import org.booklore.repository.AuthorRepository; -import org.booklore.util.FileService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class AppAuthorService { - - private static final int DEFAULT_PAGE_SIZE = 30; - private static final int MAX_PAGE_SIZE = 50; - - private final AuthorRepository authorRepository; - private final AuthenticationService authenticationService; - private final FileService fileService; - private final EntityManager entityManager; - - @Transactional(readOnly = true) - public AppPageResponse getAuthors( - Integer page, - Integer size, - String sortBy, - String sortDir, - Long libraryId, - String search, - Boolean hasPhoto) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int pageNum = page != null && page >= 0 ? page : 0; - int pageSize = size != null && size > 0 ? Math.min(size, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE; - - StringBuilder whereClause = new StringBuilder(" WHERE (1=1)"); - buildLibraryFilter(whereClause, accessibleLibraryIds, libraryId); - buildSearchFilter(whereClause, search); - - String fromClause = " FROM AuthorEntity a LEFT JOIN a.bookMetadataEntityList bm LEFT JOIN bm.book b"; - - // Count query - String countJpql = "SELECT COUNT(DISTINCT a.id)" + fromClause + whereClause; - TypedQuery countQuery = entityManager.createQuery(countJpql, Long.class); - setQueryParams(countQuery, accessibleLibraryIds, libraryId, search); - long totalElements = countQuery.getSingleResult(); - - if (totalElements == 0) { - return AppPageResponse.of(Collections.emptyList(), pageNum, pageSize, 0L); - } - - // Data query with book count - String orderClause = buildOrderClause(sortBy, sortDir); - String dataJpql = "SELECT a, COUNT(DISTINCT bm.id)" + fromClause + whereClause - + " GROUP BY a" + orderClause; - TypedQuery dataQuery = entityManager.createQuery(dataJpql, Object[].class); - setQueryParams(dataQuery, accessibleLibraryIds, libraryId, search); - dataQuery.setFirstResult(pageNum * pageSize); - dataQuery.setMaxResults(pageSize); - - List results = dataQuery.getResultList(); - - List summaries = results.stream() - .map(row -> { - AuthorEntity author = (AuthorEntity) row[0]; - long bookCount = (Long) row[1]; - boolean authorHasPhoto = Files.exists(Paths.get(fileService.getAuthorThumbnailFile(author.getId()))); - return AppAuthorSummary.builder() - .id(author.getId()) - .name(author.getName()) - .asin(author.getAsin()) - .bookCount((int) bookCount) - .hasPhoto(authorHasPhoto) - .build(); - }) - .collect(Collectors.toList()); - - // Post-filter by hasPhoto if requested - if (hasPhoto != null) { - summaries = summaries.stream() - .filter(s -> s.isHasPhoto() == hasPhoto) - .collect(Collectors.toList()); - // Adjust total count for hasPhoto filter โ€” requires a separate count - long filteredTotal = countAuthorsWithPhotoFilter(accessibleLibraryIds, libraryId, search, hasPhoto); - return AppPageResponse.of(summaries, pageNum, pageSize, filteredTotal); - } - - return AppPageResponse.of(summaries, pageNum, pageSize, totalElements); - } - - @Transactional(readOnly = true) - public AppAuthorDetail getAuthorDetail(Long authorId) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - AuthorEntity author = authorRepository.findById(authorId) - .orElseThrow(() -> ApiError.AUTHOR_NOT_FOUND.createException(authorId)); - - // Verify access for non-admin users - if (accessibleLibraryIds != null) { - if (accessibleLibraryIds.isEmpty() || !authorRepository.existsByIdAndLibraryIds(authorId, accessibleLibraryIds)) { - throw ApiError.AUTHOR_NOT_FOUND.createException(authorId); - } - } - - // Count books accessible to this user - int bookCount = countAccessibleBooks(authorId, accessibleLibraryIds); - boolean authorHasPhoto = Files.exists(Paths.get(fileService.getAuthorThumbnailFile(author.getId()))); - - return AppAuthorDetail.builder() - .id(author.getId()) - .name(author.getName()) - .description(author.getDescription()) - .asin(author.getAsin()) - .bookCount(bookCount) - .hasPhoto(authorHasPhoto) - .build(); - } - - private int countAccessibleBooks(Long authorId, Set accessibleLibraryIds) { - StringBuilder jpql = new StringBuilder( - "SELECT COUNT(DISTINCT bm.id) FROM AuthorEntity a JOIN a.bookMetadataEntityList bm JOIN bm.book b" - + " WHERE a.id = :authorId AND (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY"); - if (accessibleLibraryIds != null) { - jpql.append(" AND b.library.id IN :libraryIds"); - } - TypedQuery query = entityManager.createQuery(jpql.toString(), Long.class); - query.setParameter("authorId", authorId); - if (accessibleLibraryIds != null) { - query.setParameter("libraryIds", accessibleLibraryIds); - } - return query.getSingleResult().intValue(); - } - - private long countAuthorsWithPhotoFilter(Set accessibleLibraryIds, Long libraryId, String search, boolean hasPhoto) { - // Since hasPhoto is file-system based, we need to count all matching authors - // and check their photos. For large datasets this could be optimized with a DB column. - StringBuilder whereClause = new StringBuilder(" WHERE (1=1)"); - buildLibraryFilter(whereClause, accessibleLibraryIds, libraryId); - buildSearchFilter(whereClause, search); - - String jpql = "SELECT DISTINCT a FROM AuthorEntity a LEFT JOIN a.bookMetadataEntityList bm LEFT JOIN bm.book b" - + whereClause; - TypedQuery query = entityManager.createQuery(jpql, AuthorEntity.class); - setQueryParams(query, accessibleLibraryIds, libraryId, search); - - return query.getResultList().stream() - .filter(a -> Files.exists(Paths.get(fileService.getAuthorThumbnailFile(a.getId()))) == hasPhoto) - .count(); - } - - private void buildLibraryFilter(StringBuilder whereClause, Set accessibleLibraryIds, Long libraryId) { - if (libraryId != null) { - whereClause.append(" AND b.library.id = :libraryId"); - } else if (accessibleLibraryIds != null) { - whereClause.append(" AND b.library.id IN :libraryIds"); - } - whereClause.append(" AND (b.deleted IS NULL OR b.deleted = false)"); - whereClause.append(" AND b.bookFiles IS NOT EMPTY"); - } - - private void buildSearchFilter(StringBuilder whereClause, String search) { - if (search != null && !search.trim().isEmpty()) { - whereClause.append(" AND LOWER(a.name) LIKE :search"); - } - } - - private String buildOrderClause(String sortBy, String sortDir) { - String direction = "asc".equalsIgnoreCase(sortDir) ? "ASC" : "DESC"; - String field = switch (sortBy != null ? sortBy.toLowerCase() : "") { - case "name" -> "a.name"; - case "bookcount", "book_count" -> "COUNT(DISTINCT bm.id)"; - case "recent", "id" -> "a.id"; - default -> "a.name"; - }; - return " ORDER BY " + field + " " + direction; - } - - private void setQueryParams(TypedQuery query, Set accessibleLibraryIds, Long libraryId, String search) { - if (libraryId != null) { - query.setParameter("libraryId", libraryId); - } else if (accessibleLibraryIds != null) { - query.setParameter("libraryIds", accessibleLibraryIds); - } - if (search != null && !search.trim().isEmpty()) { - query.setParameter("search", "%" + search.trim().toLowerCase() + "%"); - } - } - - private Set getAccessibleLibraryIds(BookLoreUser user) { - if (user.getPermissions().isAdmin()) { - return null; - } - if (user.getAssignedLibraries() == null || user.getAssignedLibraries().isEmpty()) { - return Collections.emptySet(); - } - return user.getAssignedLibraries().stream() - .map(Library::getId) - .collect(Collectors.toSet()); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/service/AppBookService.java b/booklore-api/src/main/java/org/booklore/app/service/AppBookService.java deleted file mode 100644 index 60cf7f097..000000000 --- a/booklore-api/src/main/java/org/booklore/app/service/AppBookService.java +++ /dev/null @@ -1,698 +0,0 @@ -package org.booklore.app.service; - -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.ApiError; -import org.booklore.app.dto.AppBookDetail; -import org.booklore.app.dto.AppBookSummary; -import org.booklore.app.dto.AppFilterOptions; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.app.specification.AppBookSpecification; -import org.booklore.model.dto.Book; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.*; -import org.booklore.model.enums.BookFileType; -import org.booklore.model.enums.ReadStatus; -import org.booklore.repository.BookRepository; -import org.booklore.repository.ShelfRepository; -import org.booklore.repository.UserBookFileProgressRepository; -import org.booklore.repository.UserBookProgressRepository; -import org.booklore.service.opds.MagicShelfBookService; -import lombok.AllArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class AppBookService { - - private static final int DEFAULT_PAGE_SIZE = 20; - private static final int MAX_PAGE_SIZE = 50; - - private final BookRepository bookRepository; - private final UserBookProgressRepository userBookProgressRepository; - private final UserBookFileProgressRepository userBookFileProgressRepository; - private final ShelfRepository shelfRepository; - private final AuthenticationService authenticationService; - private final AppBookMapper mobileBookMapper; - private final MagicShelfBookService magicShelfBookService; - private final EntityManager entityManager; - - @Transactional(readOnly = true) - public AppPageResponse getBooks( - Integer page, - Integer size, - String sortBy, - String sortDir, - Long libraryId, - Long shelfId, - ReadStatus status, - String search, - BookFileType fileType, - Integer minRating, - Integer maxRating, - String authors, - String language) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int pageNum = page != null && page >= 0 ? page : 0; - int pageSize = size != null && size > 0 ? Math.min(size, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE; - - Sort sort = buildSort(sortBy, sortDir); - Pageable pageable = PageRequest.of(pageNum, pageSize, sort); - - Specification spec = buildSpecification( - accessibleLibraryIds, libraryId, shelfId, status, search, userId, - fileType, minRating, maxRating, authors, language); - - Page bookPage = bookRepository.findAll(spec, pageable); - - Set bookIds = bookPage.getContent().stream() - .map(BookEntity::getId) - .collect(Collectors.toSet()); - Map progressMap = getProgressMap(userId, bookIds); - - List summaries = bookPage.getContent().stream() - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - - return AppPageResponse.of(summaries, pageNum, pageSize, bookPage.getTotalElements()); - } - - @Transactional(readOnly = true) - public AppBookDetail getBookDetail(Long bookId) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - BookEntity book = bookRepository.findByIdWithBookFiles(bookId) - .orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - - if (accessibleLibraryIds != null && !accessibleLibraryIds.contains(book.getLibrary().getId())) { - throw ApiError.FORBIDDEN.createException("Access denied to this book"); - } - - UserBookProgressEntity progress = userBookProgressRepository - .findByUserIdAndBookId(userId, bookId) - .orElse(null); - - UserBookFileProgressEntity fileProgress = userBookFileProgressRepository - .findMostRecentAudiobookProgressByUserIdAndBookId(userId, bookId) - .orElse(null); - - return mobileBookMapper.toDetail(book, progress, fileProgress); - } - - @Transactional(readOnly = true) - public AppPageResponse searchBooks( - String query, - Integer page, - Integer size) { - - if (query == null || query.trim().isEmpty()) { - throw ApiError.INVALID_QUERY_PARAMETERS.createException(); - } - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int pageNum = validatePageNumber(page); - int pageSize = validatePageSize(size); - - Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "addedOn")); - - Specification spec = AppBookSpecification.combine( - AppBookSpecification.notDeleted(), - AppBookSpecification.hasDigitalFile(), - AppBookSpecification.inLibraries(accessibleLibraryIds), - AppBookSpecification.searchText(query) - ); - - Page bookPage = bookRepository.findAll(spec, pageable); - return buildPageResponse(bookPage, userId, pageNum, pageSize); - } - - @Transactional(readOnly = true) - public List getContinueReading(Integer limit) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int maxItems = validateLimit(limit, 10); - - Specification spec = AppBookSpecification.combine( - AppBookSpecification.notDeleted(), - AppBookSpecification.hasDigitalFile(), - AppBookSpecification.inLibraries(accessibleLibraryIds), - AppBookSpecification.inProgress(userId), - AppBookSpecification.hasNonAudiobookFile() - ); - - List books = bookRepository.findAll(spec); - Map progressMap = getProgressMapForBooks(userId, books); - - return books.stream() - .filter(book -> progressMap.containsKey(book.getId())) - .sorted((b1, b2) -> { - Instant t1 = progressMap.get(b1.getId()).getLastReadTime(); - Instant t2 = progressMap.get(b2.getId()).getLastReadTime(); - if (t1 == null && t2 == null) return 0; - if (t1 == null) return 1; - if (t2 == null) return -1; - return t2.compareTo(t1); - }) - .limit(maxItems) - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - } - - @Transactional(readOnly = true) - public List getContinueListening(Integer limit) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int maxItems = validateLimit(limit, 10); - - Specification spec = AppBookSpecification.combine( - AppBookSpecification.notDeleted(), - AppBookSpecification.hasDigitalFile(), - AppBookSpecification.inLibraries(accessibleLibraryIds), - AppBookSpecification.inProgress(userId), - AppBookSpecification.hasAudiobookFile() - ); - - List books = bookRepository.findAll(spec); - Map progressMap = getProgressMapForBooks(userId, books); - - return books.stream() - .filter(book -> progressMap.containsKey(book.getId())) - .sorted((b1, b2) -> { - Instant t1 = progressMap.get(b1.getId()).getLastReadTime(); - Instant t2 = progressMap.get(b2.getId()).getLastReadTime(); - if (t1 == null && t2 == null) return 0; - if (t1 == null) return 1; - if (t2 == null) return -1; - return t2.compareTo(t1); - }) - .limit(maxItems) - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - } - - @Transactional(readOnly = true) - public List getRecentlyAdded(Integer limit) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int maxItems = validateLimit(limit, 10); - - Specification spec = AppBookSpecification.combine( - AppBookSpecification.notDeleted(), - AppBookSpecification.hasDigitalFile(), - AppBookSpecification.inLibraries(accessibleLibraryIds), - AppBookSpecification.addedWithinDays(30) - ); - - Pageable pageable = PageRequest.of(0, maxItems, Sort.by(Sort.Direction.DESC, "addedOn")); - Page bookPage = bookRepository.findAll(spec, pageable); - Map progressMap = getProgressMapForBooks(userId, bookPage.getContent()); - - return bookPage.getContent().stream() - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - } - - @Transactional(readOnly = true) - public List getRecentlyScanned(Integer limit) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int maxItems = validateLimit(limit, 10); - - Specification spec = AppBookSpecification.combine( - AppBookSpecification.notDeleted(), - AppBookSpecification.hasScannedOn(), - AppBookSpecification.inLibraries(accessibleLibraryIds) - ); - - Pageable pageable = PageRequest.of(0, maxItems, Sort.by(Sort.Direction.DESC, "scannedOn")); - Page bookPage = bookRepository.findAll(spec, pageable); - Map progressMap = getProgressMapForBooks(userId, bookPage.getContent()); - - return bookPage.getContent().stream() - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - } - - @Transactional(readOnly = true) - public AppPageResponse getRandomBooks( - Integer page, - Integer size, - Long libraryId) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - int pageNum = validatePageNumber(page); - int pageSize = validatePageSize(size); - - Specification spec = buildBaseSpecification(accessibleLibraryIds, libraryId); - - long totalElements = bookRepository.count(spec); - - if (totalElements == 0) { - return AppPageResponse.of(Collections.emptyList(), pageNum, pageSize, 0L); - } - - long maxOffset = Math.max(0, totalElements - pageSize); - int randomOffset = ThreadLocalRandom.current().nextInt((int) maxOffset + 1); - - Pageable pageable = PageRequest.of(randomOffset / pageSize, pageSize); - Page bookPage = bookRepository.findAll(spec, pageable); - - return buildPageResponse(bookPage, userId, pageNum, pageSize); - } - - @Transactional(readOnly = true) - public AppPageResponse getBooksByMagicShelf( - Long magicShelfId, - Integer page, - Integer size) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - - int pageNum = validatePageNumber(page); - int pageSize = validatePageSize(size); - - var booksPage = magicShelfBookService.getBooksByMagicShelfId(userId, magicShelfId, pageNum, pageSize); - - Set bookIds = booksPage.getContent().stream() - .map(Book::getId) - .collect(Collectors.toSet()); - - if (bookIds.isEmpty()) { - return AppPageResponse.of(Collections.emptyList(), pageNum, pageSize, 0L); - } - - List bookEntities = bookRepository.findAllById(bookIds); - Map progressMap = getProgressMapForBooks(userId, bookEntities); - - List summaries = bookEntities.stream() - .filter(BookEntity::hasFiles) - .map(bookEntity -> mobileBookMapper.toSummary(bookEntity, progressMap.get(bookEntity.getId()))) - .collect(Collectors.toList()); - - return AppPageResponse.of(summaries, pageNum, pageSize, booksPage.getTotalElements()); - } - - @Transactional(readOnly = true) - public AppFilterOptions getFilterOptions(Long libraryId, Long shelfId, Long magicShelfId) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - // Resolve magic shelf to a set of book IDs if requested - Set magicBookIds = null; - if (magicShelfId != null) { - magicBookIds = resolveMagicShelfBookIds(magicShelfId, userId); - if (magicBookIds.isEmpty()) { - return AppFilterOptions.builder() - .authors(Collections.emptyList()) - .languages(Collections.emptyList()) - .fileTypes(Collections.emptyList()) - .readStatuses(getReadStatusOptions()) - .build(); - } - } - - // Validate library access - if (libraryId != null && accessibleLibraryIds != null && !accessibleLibraryIds.contains(libraryId)) { - throw ApiError.FORBIDDEN.createException("Access denied to library " + libraryId); - } - - // Validate shelf access - if (shelfId != null) { - ShelfEntity shelf = shelfRepository.findById(shelfId) - .orElseThrow(() -> ApiError.SHELF_NOT_FOUND.createException(shelfId)); - if (!shelf.isPublic() && !shelf.getUser().getId().equals(userId)) { - throw ApiError.FORBIDDEN.createException("Access denied to shelf " + shelfId); - } - } - - // Build scoping clauses - String libraryClause = ""; - String shelfClause = ""; - String magicBookClause = ""; - - if (magicBookIds != null) { - magicBookClause = "AND b.id IN :magicBookIds"; - } else if (shelfId != null) { - shelfClause = "AND b.id IN (SELECT sb.id FROM ShelfEntity s JOIN s.bookEntities sb WHERE s.id = :shelfId)"; - } - - if (libraryId != null) { - libraryClause = "AND b.library.id = :libraryId"; - } else if (accessibleLibraryIds != null) { - libraryClause = "AND b.library.id IN :libraryIds"; - } - - // Build the optional WHERE suffix once โ€” each clause already starts with "AND" - String scopeClause = buildScopeClause(libraryClause, shelfClause, magicBookClause); - - // Authors with book count (top 200 by count) - String authorQuery = "SELECT a.name, COUNT(DISTINCT b.id) FROM BookEntity b" - + " JOIN b.metadata m JOIN m.authors a" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + scopeClause - + " GROUP BY a.name ORDER BY COUNT(DISTINCT b.id) DESC"; - var authorQ = entityManager.createQuery(authorQuery, Tuple.class); - setFilterQueryParams(authorQ, accessibleLibraryIds, libraryId, shelfId, magicBookIds); - authorQ.setMaxResults(200); - - List authors = authorQ.getResultList().stream() - .map(t -> AppFilterOptions.AuthorOption.builder() - .name(t.get(0, String.class)) - .count(t.get(1, Long.class)) - .build()) - .toList(); - - // Languages with book count - String langQuery = "SELECT m.language, COUNT(DISTINCT b.id) FROM BookEntity b" - + " JOIN b.metadata m" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND m.language IS NOT NULL AND m.language <> ''" - + scopeClause - + " GROUP BY m.language ORDER BY COUNT(DISTINCT b.id) DESC"; - var langQ = entityManager.createQuery(langQuery, Tuple.class); - setFilterQueryParams(langQ, accessibleLibraryIds, libraryId, shelfId, magicBookIds); - - List languages = langQ.getResultList().stream() - .map(t -> AppFilterOptions.LanguageOption.builder() - .code(t.get(0, String.class)) - .label(Locale.forLanguageTag(t.get(0, String.class)).getDisplayLanguage(Locale.ENGLISH)) - .count(t.get(1, Long.class)) - .build()) - .toList(); - - // Distinct file types present in scoped books - String fileTypeQuery = "SELECT DISTINCT bf.bookType FROM BookEntity b" - + " JOIN b.bookFiles bf" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND bf.isBookFormat = true" - + scopeClause; - var ftQ = entityManager.createQuery(fileTypeQuery, BookFileType.class); - setFilterQueryParams(ftQ, accessibleLibraryIds, libraryId, shelfId, magicBookIds); - - List fileTypes = ftQ.getResultList().stream() - .map(Enum::name) - .sorted() - .toList(); - - // Read statuses โ€” return all meaningful values - List readStatuses = getReadStatusOptions(); - - return AppFilterOptions.builder() - .authors(authors) - .languages(languages) - .fileTypes(fileTypes) - .readStatuses(readStatuses) - .build(); - } - - private String buildScopeClause(String libraryClause, String shelfClause, String magicBookClause) { - var sb = new StringBuilder(); - if (!libraryClause.isEmpty()) sb.append(" ").append(libraryClause); - if (!shelfClause.isEmpty()) sb.append(" ").append(shelfClause); - if (!magicBookClause.isEmpty()) sb.append(" ").append(magicBookClause); - return sb.toString(); - } - - private void setFilterQueryParams(jakarta.persistence.Query query, Set accessibleLibraryIds, Long libraryId, Long shelfId, Set magicBookIds) { - if (libraryId != null) { - query.setParameter("libraryId", libraryId); - } else if (accessibleLibraryIds != null) { - query.setParameter("libraryIds", accessibleLibraryIds); - } - if (shelfId != null && magicBookIds == null) { - query.setParameter("shelfId", shelfId); - } - if (magicBookIds != null) { - query.setParameter("magicBookIds", magicBookIds); - } - } - - private Set resolveMagicShelfBookIds(Long magicShelfId, Long userId) { - // Reuse MagicShelfBookService which already handles access validation, - // rule evaluation, and library filtering. - var booksPage = magicShelfBookService.getBooksByMagicShelfId(userId, magicShelfId, 0, 10000); - return booksPage.getContent().stream() - .map(Book::getId) - .collect(Collectors.toSet()); - } - - private List getReadStatusOptions() { - return Arrays.stream(ReadStatus.values()) - .filter(s -> s != ReadStatus.UNSET) - .map(Enum::name) - .toList(); - } - - @Transactional - public void updateReadStatus(Long bookId, ReadStatus status) { - UserBookProgressEntity progress = validateAccessAndGetProgress(bookId); - - progress.setReadStatus(status); - progress.setReadStatusModifiedTime(Instant.now()); - - if (status == ReadStatus.READ && progress.getDateFinished() == null) { - progress.setDateFinished(Instant.now()); - } - - userBookProgressRepository.save(progress); - } - - @Transactional - public void updatePersonalRating(Long bookId, Integer rating) { - UserBookProgressEntity progress = validateAccessAndGetProgress(bookId); - - progress.setPersonalRating(rating); - userBookProgressRepository.save(progress); - } - - private UserBookProgressEntity validateAccessAndGetProgress(Long bookId) { - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - BookEntity book = bookRepository.findById(bookId) - .orElseThrow(() -> ApiError.BOOK_NOT_FOUND.createException(bookId)); - - validateLibraryAccess(accessibleLibraryIds, book.getLibrary().getId()); - - return userBookProgressRepository - .findByUserIdAndBookId(userId, bookId) - .orElseGet(() -> createNewProgress(userId, book)); - } - - private void validateLibraryAccess(Set accessibleLibraryIds, Long libraryId) { - if (accessibleLibraryIds != null && !accessibleLibraryIds.contains(libraryId)) { - throw ApiError.FORBIDDEN.createException("Access denied to this book"); - } - } - - private UserBookProgressEntity createNewProgress(Long userId, BookEntity book) { - return UserBookProgressEntity.builder() - .user(BookLoreUserEntity.builder().id(userId).build()) - .book(book) - .build(); - } - - private Set getAccessibleLibraryIds(BookLoreUser user) { - if (user.getPermissions().isAdmin()) { - return null; - } - if (user.getAssignedLibraries() == null || user.getAssignedLibraries().isEmpty()) { - return Collections.emptySet(); - } - return user.getAssignedLibraries().stream() - .map(Library::getId) - .collect(Collectors.toSet()); - } - - private Map getProgressMap(Long userId, Set bookIds) { - if (bookIds.isEmpty()) { - return Collections.emptyMap(); - } - return userBookProgressRepository.findByUserIdAndBookIdIn(userId, bookIds).stream() - .collect(Collectors.toMap( - p -> p.getBook().getId(), - Function.identity() - )); - } - - private Specification buildSpecification( - Set accessibleLibraryIds, - Long libraryId, - Long shelfId, - ReadStatus status, - String search, - Long userId, - BookFileType fileType, - Integer minRating, - Integer maxRating, - String authors, - String language) { - - List> specs = new ArrayList<>(); - specs.add(AppBookSpecification.notDeleted()); - specs.add(AppBookSpecification.hasDigitalFile()); - - if (accessibleLibraryIds != null) { - if (libraryId != null && accessibleLibraryIds.contains(libraryId)) { - specs.add(AppBookSpecification.inLibrary(libraryId)); - } else if (libraryId != null) { - throw ApiError.FORBIDDEN.createException("Access denied to library " + libraryId); - } else { - specs.add(AppBookSpecification.inLibraries(accessibleLibraryIds)); - } - } else if (libraryId != null) { - specs.add(AppBookSpecification.inLibrary(libraryId)); - } - - if (shelfId != null) { - ShelfEntity shelf = shelfRepository.findById(shelfId) - .orElseThrow(() -> ApiError.SHELF_NOT_FOUND.createException(shelfId)); - if (!shelf.isPublic() && !shelf.getUser().getId().equals(userId)) { - throw ApiError.FORBIDDEN.createException("Access denied to shelf " + shelfId); - } - specs.add(AppBookSpecification.inShelf(shelfId)); - } - - if (status != null) { - specs.add(AppBookSpecification.withReadStatus(status, userId)); - } - - if (search != null && !search.trim().isEmpty()) { - specs.add(AppBookSpecification.searchText(search)); - } - - if (fileType != null) { - specs.add(AppBookSpecification.withFileType(fileType)); - } - - if (minRating != null) { - specs.add(AppBookSpecification.withMinRating(minRating, userId)); - } - - if (maxRating != null) { - specs.add(AppBookSpecification.withMaxRating(maxRating, userId)); - } - - if (authors != null && !authors.trim().isEmpty()) { - specs.add(AppBookSpecification.withAuthor(authors.trim())); - } - - if (language != null && !language.trim().isEmpty()) { - specs.add(AppBookSpecification.withLanguage(language.trim())); - } - - return AppBookSpecification.combine(specs.toArray(new Specification[0])); - } - - private Sort buildSort(String sortBy, String sortDir) { - Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) - ? Sort.Direction.ASC - : Sort.Direction.DESC; - - String field = switch (sortBy != null ? sortBy.toLowerCase() : "") { - case "title" -> "metadata.title"; - case "seriesname", "series" -> "metadata.seriesName"; - case "lastreadtime" -> "addedOn"; - default -> "addedOn"; - }; - - return Sort.by(direction, field); - } - - private int validatePageNumber(Integer page) { - return page != null && page >= 0 ? page : 0; - } - - private int validatePageSize(Integer size) { - return size != null && size > 0 ? Math.min(size, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE; - } - - private int validateLimit(Integer limit, int defaultValue) { - return limit != null && limit > 0 ? Math.min(limit, MAX_PAGE_SIZE) : defaultValue; - } - - private Specification buildBaseSpecification(Set accessibleLibraryIds, Long libraryId) { - List> specs = new ArrayList<>(); - specs.add(AppBookSpecification.notDeleted()); - specs.add(AppBookSpecification.hasDigitalFile()); - - if (accessibleLibraryIds != null) { - if (libraryId != null && !accessibleLibraryIds.contains(libraryId)) { - throw ApiError.FORBIDDEN.createException("Access denied to library " + libraryId); - } - specs.add(libraryId != null - ? AppBookSpecification.inLibrary(libraryId) - : AppBookSpecification.inLibraries(accessibleLibraryIds)); - } else if (libraryId != null) { - specs.add(AppBookSpecification.inLibrary(libraryId)); - } - - return AppBookSpecification.combine(specs.toArray(new Specification[0])); - } - - private AppPageResponse buildPageResponse( - Page bookPage, - Long userId, - int pageNum, - int pageSize) { - - Map progressMap = getProgressMapForBooks(userId, bookPage.getContent()); - - List summaries = bookPage.getContent().stream() - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .collect(Collectors.toList()); - - return AppPageResponse.of(summaries, pageNum, pageSize, bookPage.getTotalElements()); - } - - private Map getProgressMapForBooks(Long userId, List books) { - if (books.isEmpty()) { - return Collections.emptyMap(); - } - Set bookIds = books.stream() - .map(BookEntity::getId) - .collect(Collectors.toSet()); - return getProgressMap(userId, bookIds); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/service/AppNotebookService.java b/booklore-api/src/main/java/org/booklore/app/service/AppNotebookService.java deleted file mode 100644 index 8937b5bd3..000000000 --- a/booklore-api/src/main/java/org/booklore/app/service/AppNotebookService.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.booklore.app.service; - -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.app.dto.AppNotebookBookSummary; -import org.booklore.app.dto.AppNotebookEntry; -import org.booklore.app.dto.AppNotebookUpdateRequest; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.model.dto.UpdateAnnotationRequest; -import org.booklore.model.dto.UpdateBookMarkRequest; -import org.booklore.model.dto.UpdateBookNoteV2Request; -import org.booklore.repository.AuthorRepository; -import org.booklore.repository.NotebookEntryRepository; -import org.booklore.service.book.AnnotationService; -import org.booklore.service.book.BookMarkService; -import org.booklore.service.book.BookNoteV2Service; -import lombok.AllArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class AppNotebookService { - - private final NotebookEntryRepository notebookEntryRepository; - private final AuthorRepository authorRepository; - private final AuthenticationService authenticationService; - private final AnnotationService annotationService; - private final BookNoteV2Service bookNoteV2Service; - private final BookMarkService bookMarkService; - - @Transactional(readOnly = true) - public AppPageResponse getBooksWithAnnotations(int page, int size, String search) { - Long userId = authenticationService.getAuthenticatedUser().getId(); - Pageable pageable = PageRequest.of(page, size); - - Page booksPage = - notebookEntryRepository.findBooksWithAnnotationsPaginated(userId, wrapSearch(search), pageable); - - List content = booksPage.getContent(); - if (content.isEmpty()) { - return AppPageResponse.of(List.of(), page, size, 0); - } - - Set bookIds = content.stream() - .map(NotebookEntryRepository.BookWithCountProjection::getBookId) - .collect(Collectors.toSet()); - - Map> authorsByBook = authorRepository.findAuthorNamesByBookIds(bookIds) - .stream() - .collect(Collectors.groupingBy( - AuthorRepository.AuthorBookProjection::getBookId, - Collectors.mapping(AuthorRepository.AuthorBookProjection::getAuthorName, Collectors.toList()))); - - List summaries = content.stream() - .map(p -> AppNotebookBookSummary.builder() - .bookId(p.getBookId()) - .bookTitle(p.getBookTitle()) - .noteCount(p.getNoteCount()) - .authors(authorsByBook.getOrDefault(p.getBookId(), List.of())) - .coverUpdatedOn(p.getCoverUpdatedOn()) - .build()) - .toList(); - - return AppPageResponse.of(summaries, page, size, booksPage.getTotalElements()); - } - - @Transactional(readOnly = true) - public AppPageResponse getEntriesForBook(Long bookId, int page, int size, - Set types, String search, String sort) { - Long userId = authenticationService.getAuthenticatedUser().getId(); - Set entryTypes = (types == null || types.isEmpty()) - ? Set.of("HIGHLIGHT", "NOTE", "BOOKMARK") - : types; - Pageable pageable = PageRequest.of(page, size, toSort(sort)); - - Page entriesPage = - notebookEntryRepository.findEntries(userId, entryTypes, bookId, wrapSearch(search), pageable); - - List entries = entriesPage.getContent().stream() - .map(AppNotebookService::toMobileEntry) - .toList(); - - return AppPageResponse.of(entries, page, size, entriesPage.getTotalElements()); - } - - @Transactional - public AppNotebookEntry updateEntry(Long entryId, String type, AppNotebookUpdateRequest request) { - return switch (type.toUpperCase()) { - case "HIGHLIGHT" -> { - var updateReq = new UpdateAnnotationRequest(); - if (request.getNote() != null) updateReq.setNote(request.getNote()); - if (request.getColor() != null) updateReq.setColor(request.getColor()); - var result = annotationService.updateAnnotation(entryId, updateReq); - yield AppNotebookEntry.builder() - .id(result.getId()) - .type("HIGHLIGHT") - .bookId(result.getBookId()) - .text(result.getText()) - .note(result.getNote()) - .color(result.getColor()) - .style(result.getStyle()) - .chapterTitle(result.getChapterTitle()) - .createdAt(result.getCreatedAt()) - .updatedAt(result.getUpdatedAt()) - .build(); - } - case "NOTE" -> { - var updateReq = new UpdateBookNoteV2Request(); - if (request.getNote() != null) updateReq.setNoteContent(request.getNote()); - if (request.getColor() != null) updateReq.setColor(request.getColor()); - var result = bookNoteV2Service.updateNote(entryId, updateReq); - yield AppNotebookEntry.builder() - .id(result.getId()) - .type("NOTE") - .bookId(result.getBookId()) - .text(result.getSelectedText()) - .note(result.getNoteContent()) - .color(result.getColor()) - .chapterTitle(result.getChapterTitle()) - .createdAt(result.getCreatedAt()) - .updatedAt(result.getUpdatedAt()) - .build(); - } - case "BOOKMARK" -> { - var updateReq = new UpdateBookMarkRequest(); - if (request.getNote() != null) updateReq.setNotes(request.getNote()); - if (request.getColor() != null) updateReq.setColor(request.getColor()); - var result = bookMarkService.updateBookmark(entryId, updateReq); - yield AppNotebookEntry.builder() - .id(result.getId()) - .type("BOOKMARK") - .bookId(result.getBookId()) - .text(result.getTitle()) - .note(result.getNotes()) - .color(result.getColor()) - .createdAt(result.getCreatedAt()) - .updatedAt(result.getUpdatedAt()) - .build(); - } - default -> throw new IllegalArgumentException("Unknown entry type: " + type); - }; - } - - @Transactional - public void deleteEntry(Long entryId, String type) { - switch (type.toUpperCase()) { - case "HIGHLIGHT" -> annotationService.deleteAnnotation(entryId); - case "NOTE" -> bookNoteV2Service.deleteNote(entryId); - case "BOOKMARK" -> bookMarkService.deleteBookmark(entryId); - default -> throw new IllegalArgumentException("Unknown entry type: " + type); - } - } - - private String wrapSearch(String search) { - if (search == null || search.isBlank()) return null; - return "%" + escapeLike(search) + "%"; - } - - private static String escapeLike(String input) { - return input.trim() - .replace("\\", "\\\\") - .replace("%", "\\%") - .replace("_", "\\_"); - } - - private static Sort toSort(String sort) { - if ("chapter".equalsIgnoreCase(sort)) { - return Sort.by(Sort.Order.asc("chapterTitle"), Sort.Order.asc("createdAt")); - } - if ("date_asc".equalsIgnoreCase(sort)) { - return Sort.by("createdAt").ascending(); - } - return Sort.by("createdAt").descending(); - } - - private static AppNotebookEntry toMobileEntry(NotebookEntryRepository.EntryProjection p) { - return AppNotebookEntry.builder() - .id(p.getId()) - .type(p.getType()) - .bookId(p.getBookId()) - .text(p.getText()) - .note(p.getNote()) - .color(p.getColor()) - .style(p.getStyle()) - .chapterTitle(p.getChapterTitle()) - .createdAt(p.getCreatedAt()) - .updatedAt(p.getUpdatedAt()) - .build(); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java b/booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java deleted file mode 100644 index 57b6258d6..000000000 --- a/booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java +++ /dev/null @@ -1,390 +0,0 @@ -package org.booklore.app.service; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; -import lombok.AllArgsConstructor; -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.ApiError; -import org.booklore.app.dto.*; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.app.specification.AppBookSpecification; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.*; -import org.booklore.model.enums.BookFileType; -import org.booklore.repository.BookRepository; -import org.booklore.repository.UserBookProgressRepository; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.Instant; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; - -@Service -@AllArgsConstructor -public class AppSeriesService { - - private static final int DEFAULT_PAGE_SIZE = 20; - private static final int MAX_PAGE_SIZE = 50; - - private final EntityManager entityManager; - private final AuthenticationService authenticationService; - private final BookRepository bookRepository; - private final UserBookProgressRepository userBookProgressRepository; - private final AppBookMapper mobileBookMapper; - - @Transactional(readOnly = true) - public AppPageResponse getSeries( - Integer page, - Integer size, - String sortBy, - String sortDir, - Long libraryId, - String search, - boolean inProgressOnly) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - if (libraryId != null) { - validateLibraryAccess(accessibleLibraryIds, libraryId); - } - - int pageNum = page != null && page >= 0 ? page : 0; - int pageSize = size != null && size > 0 ? Math.min(size, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE; - - // Build WHERE clause fragments - String libraryClause = buildLibraryClause(accessibleLibraryIds, libraryId); - String searchClause = (search != null && !search.trim().isEmpty()) - ? " AND LOWER(m.seriesName) LIKE :searchPattern" - : ""; - - String havingClause = inProgressOnly - ? " HAVING SUM(CASE WHEN p.readStatus IN (org.booklore.model.enums.ReadStatus.READING, org.booklore.model.enums.ReadStatus.RE_READING) THEN 1 ELSE 0 END) > 0" - : ""; - - String orderBy = buildSeriesOrderBy(sortBy, sortDir, inProgressOnly); - - // Phase 1: Aggregate query - String aggregateQuery = "SELECT m.seriesName, COUNT(b.id), MAX(m.seriesTotal), MAX(b.addedOn)," - + " SUM(CASE WHEN p.readStatus = org.booklore.model.enums.ReadStatus.READ THEN 1 ELSE 0 END)" - + (inProgressOnly ? ", MAX(p.lastReadTime)" : "") - + " FROM BookEntity b JOIN b.metadata m" - + " LEFT JOIN b.userBookProgress p ON p.user.id = :userId" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND m.seriesName IS NOT NULL" - + libraryClause - + searchClause - + " GROUP BY m.seriesName" - + havingClause - + " ORDER BY " + orderBy; - - var aggregateQ = entityManager.createQuery(aggregateQuery, Tuple.class); - aggregateQ.setParameter("userId", userId); - setLibraryParams(aggregateQ, accessibleLibraryIds, libraryId); - if (!searchClause.isEmpty()) { - aggregateQ.setParameter("searchPattern", "%" + search.trim().toLowerCase() + "%"); - } - aggregateQ.setFirstResult(pageNum * pageSize); - aggregateQ.setMaxResults(pageSize); - - List aggregateResults = aggregateQ.getResultList(); - - // Count query - String countQuery = "SELECT COUNT(DISTINCT m.seriesName) FROM BookEntity b JOIN b.metadata m" - + (inProgressOnly ? " LEFT JOIN b.userBookProgress p ON p.user.id = :userId" : "") - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND m.seriesName IS NOT NULL" - + libraryClause - + searchClause; - - if (inProgressOnly) { - // For in-progress, we need the HAVING filter in the count โ€” use a subquery approach - String countWithHaving = "SELECT COUNT(*) FROM (" - + "SELECT m.seriesName FROM BookEntity b JOIN b.metadata m" - + " LEFT JOIN b.userBookProgress p ON p.user.id = :userId" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND m.seriesName IS NOT NULL" - + libraryClause - + searchClause - + " GROUP BY m.seriesName" - + " HAVING SUM(CASE WHEN p.readStatus IN (org.booklore.model.enums.ReadStatus.READING, org.booklore.model.enums.ReadStatus.RE_READING) THEN 1 ELSE 0 END) > 0" - + ")"; - // JPQL doesn't support subqueries in FROM โ€” count via result list size instead - String countAlt = "SELECT m.seriesName FROM BookEntity b JOIN b.metadata m" - + " LEFT JOIN b.userBookProgress p ON p.user.id = :userId" - + " WHERE (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + " AND m.seriesName IS NOT NULL" - + libraryClause - + searchClause - + " GROUP BY m.seriesName" - + " HAVING SUM(CASE WHEN p.readStatus IN (org.booklore.model.enums.ReadStatus.READING, org.booklore.model.enums.ReadStatus.RE_READING) THEN 1 ELSE 0 END) > 0"; - var countQ = entityManager.createQuery(countAlt, String.class); - countQ.setParameter("userId", userId); - setLibraryParams(countQ, accessibleLibraryIds, libraryId); - if (!searchClause.isEmpty()) { - countQ.setParameter("searchPattern", "%" + search.trim().toLowerCase() + "%"); - } - long totalElements = countQ.getResultList().size(); - return buildSeriesPage(aggregateResults, userId, accessibleLibraryIds, libraryId, inProgressOnly, pageNum, pageSize, totalElements); - } - - var countQ = entityManager.createQuery(countQuery, Long.class); - if (inProgressOnly) { - countQ.setParameter("userId", userId); - } - setLibraryParams(countQ, accessibleLibraryIds, libraryId); - if (!searchClause.isEmpty()) { - countQ.setParameter("searchPattern", "%" + search.trim().toLowerCase() + "%"); - } - long totalElements = countQ.getSingleResult(); - - return buildSeriesPage(aggregateResults, userId, accessibleLibraryIds, libraryId, inProgressOnly, pageNum, pageSize, totalElements); - } - - private AppPageResponse buildSeriesPage( - List aggregateResults, - Long userId, - Set accessibleLibraryIds, - Long libraryId, - boolean inProgressOnly, - int pageNum, - int pageSize, - long totalElements) { - - if (aggregateResults.isEmpty()) { - return AppPageResponse.of(Collections.emptyList(), pageNum, pageSize, totalElements); - } - - List seriesNames = aggregateResults.stream() - .map(t -> t.get(0, String.class)) - .toList(); - - // Phase 2: Fetch books for enrichment - String libraryClause = buildLibraryClause(accessibleLibraryIds, libraryId); - String booksQuery = "SELECT b FROM BookEntity b" - + " JOIN FETCH b.metadata m LEFT JOIN FETCH m.authors" - + " LEFT JOIN FETCH b.bookFiles" - + " WHERE m.seriesName IN :seriesNames" - + " AND (b.deleted IS NULL OR b.deleted = false)" - + " AND b.bookFiles IS NOT EMPTY" - + libraryClause; - - var booksQ = entityManager.createQuery(booksQuery, BookEntity.class); - booksQ.setParameter("seriesNames", seriesNames); - setLibraryParams(booksQ, accessibleLibraryIds, libraryId); - - List books = booksQ.getResultList(); - - // Group books by series name - Map> booksBySeries = books.stream() - .collect(Collectors.groupingBy(b -> b.getMetadata().getSeriesName())); - - // Build aggregates map from Phase 1 - Map aggregateMap = new LinkedHashMap<>(); - for (Tuple t : aggregateResults) { - aggregateMap.put(t.get(0, String.class), t); - } - - // Merge into summaries, preserving Phase 1 order - List summaries = new ArrayList<>(); - for (String seriesName : seriesNames) { - Tuple agg = aggregateMap.get(seriesName); - List seriesBooks = booksBySeries.getOrDefault(seriesName, Collections.emptyList()); - - // Distinct authors across all books in series - List authors = seriesBooks.stream() - .filter(b -> b.getMetadata() != null && b.getMetadata().getAuthors() != null) - .flatMap(b -> b.getMetadata().getAuthors().stream()) - .map(AuthorEntity::getName) - .distinct() - .toList(); - - // Cover books sorted by seriesNumber ASC nulls last - List coverBooks = seriesBooks.stream() - .sorted(Comparator.comparing( - (BookEntity b) -> b.getMetadata().getSeriesNumber(), - Comparator.nullsLast(Comparator.naturalOrder()))) - .map(b -> { - BookFileEntity primaryFile = b.getPrimaryBookFile(); - String fileType = (primaryFile != null && primaryFile.getBookType() != null) - ? primaryFile.getBookType().name() - : null; - return SeriesCoverBook.builder() - .bookId(b.getId()) - .coverUpdatedOn(b.getMetadata().getCoverUpdatedOn()) - .seriesNumber(b.getMetadata().getSeriesNumber()) - .primaryFileType(fileType) - .build(); - }) - .toList(); - - Long booksReadLong = agg.get(4, Long.class); - int booksRead = booksReadLong != null ? booksReadLong.intValue() : 0; - - summaries.add(AppSeriesSummary.builder() - .seriesName(agg.get(0, String.class)) - .bookCount(agg.get(1, Long.class).intValue()) - .seriesTotal(agg.get(2, Integer.class)) - .latestAddedOn(agg.get(3, Instant.class)) - .booksRead(booksRead) - .authors(authors) - .coverBooks(coverBooks) - .build()); - } - - return AppPageResponse.of(summaries, pageNum, pageSize, totalElements); - } - - @Transactional(readOnly = true) - public AppPageResponse getSeriesBooks( - String seriesName, - Integer page, - Integer size, - String sortBy, - String sortDir, - Long libraryId) { - - BookLoreUser user = authenticationService.getAuthenticatedUser(); - Long userId = user.getId(); - Set accessibleLibraryIds = getAccessibleLibraryIds(user); - - if (libraryId != null) { - validateLibraryAccess(accessibleLibraryIds, libraryId); - } - - int pageNum = page != null && page >= 0 ? page : 0; - int pageSize = size != null && size > 0 ? Math.min(size, MAX_PAGE_SIZE) : DEFAULT_PAGE_SIZE; - - Sort sort = buildBookSort(sortBy, sortDir); - Pageable pageable = PageRequest.of(pageNum, pageSize, sort); - - Specification spec = buildSeriesBooksSpec(accessibleLibraryIds, libraryId, seriesName); - - Page bookPage = bookRepository.findAll(spec, pageable); - - Set bookIds = bookPage.getContent().stream() - .map(BookEntity::getId) - .collect(Collectors.toSet()); - Map progressMap = getProgressMap(userId, bookIds); - - List summaries = bookPage.getContent().stream() - .map(book -> mobileBookMapper.toSummary(book, progressMap.get(book.getId()))) - .toList(); - - return AppPageResponse.of(summaries, pageNum, pageSize, bookPage.getTotalElements()); - } - - // --- Access control helpers (duplicated from AppBookService to minimize blast radius) --- - - private Set getAccessibleLibraryIds(BookLoreUser user) { - if (user.getPermissions().isAdmin()) { - return null; - } - if (user.getAssignedLibraries() == null || user.getAssignedLibraries().isEmpty()) { - return Collections.emptySet(); - } - return user.getAssignedLibraries().stream() - .map(Library::getId) - .collect(Collectors.toSet()); - } - - private void validateLibraryAccess(Set accessibleLibraryIds, Long libraryId) { - if (accessibleLibraryIds != null && !accessibleLibraryIds.contains(libraryId)) { - throw ApiError.FORBIDDEN.createException("Access denied to library " + libraryId); - } - } - - // --- Query helpers --- - - private String buildLibraryClause(Set accessibleLibraryIds, Long libraryId) { - if (libraryId != null) { - return " AND b.library.id = :libraryId"; - } else if (accessibleLibraryIds != null) { - return " AND b.library.id IN :libraryIds"; - } - return ""; - } - - private void setLibraryParams(jakarta.persistence.Query query, Set accessibleLibraryIds, Long libraryId) { - if (libraryId != null) { - query.setParameter("libraryId", libraryId); - } else if (accessibleLibraryIds != null) { - query.setParameter("libraryIds", accessibleLibraryIds); - } - } - - private String buildSeriesOrderBy(String sortBy, String sortDir, boolean inProgressOnly) { - String dir = "asc".equalsIgnoreCase(sortDir) ? "ASC" : "DESC"; - String nullsClause = "ASC".equals(dir) ? " NULLS LAST" : " NULLS FIRST"; - - return switch (sortBy != null ? sortBy.toLowerCase() : "") { - case "name" -> "m.seriesName " + dir; - case "bookcount" -> "COUNT(b.id) " + dir; - case "readprogress" -> "SUM(CASE WHEN p.readStatus = org.booklore.model.enums.ReadStatus.READ THEN 1 ELSE 0 END) " + dir; - case "lastreadtime" -> { - if (inProgressOnly) { - yield "MAX(p.lastReadTime) " + dir + nullsClause; - } - yield "MAX(b.addedOn) " + dir + nullsClause; - } - default -> { - if (inProgressOnly) { - yield "MAX(p.lastReadTime) " + dir + nullsClause; - } - yield "MAX(b.addedOn) " + dir + nullsClause; - } - }; - } - - private Sort buildBookSort(String sortBy, String sortDir) { - Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) ? Sort.Direction.ASC : Sort.Direction.DESC; - String field = switch (sortBy != null ? sortBy.toLowerCase() : "") { - case "title" -> "metadata.title"; - case "seriesnumber" -> "metadata.seriesNumber"; - case "recentlyadded" -> "addedOn"; - default -> "metadata.seriesNumber"; - }; - return Sort.by(direction, field); - } - - private Specification buildSeriesBooksSpec(Set accessibleLibraryIds, Long libraryId, String seriesName) { - List> specs = new ArrayList<>(); - specs.add(AppBookSpecification.notDeleted()); - specs.add(AppBookSpecification.hasDigitalFile()); - specs.add(AppBookSpecification.inSeries(seriesName)); - - if (accessibleLibraryIds != null) { - specs.add(libraryId != null - ? AppBookSpecification.inLibrary(libraryId) - : AppBookSpecification.inLibraries(accessibleLibraryIds)); - } else if (libraryId != null) { - specs.add(AppBookSpecification.inLibrary(libraryId)); - } - - return AppBookSpecification.combine(specs.toArray(new Specification[0])); - } - - private Map getProgressMap(Long userId, Set bookIds) { - if (bookIds.isEmpty()) { - return Collections.emptyMap(); - } - return userBookProgressRepository.findByUserIdAndBookIdIn(userId, bookIds).stream() - .collect(Collectors.toMap( - p -> p.getBook().getId(), - Function.identity() - )); - } -} diff --git a/booklore-api/src/main/java/org/booklore/app/specification/AppBookSpecification.java b/booklore-api/src/main/java/org/booklore/app/specification/AppBookSpecification.java deleted file mode 100644 index d707b1d4e..000000000 --- a/booklore-api/src/main/java/org/booklore/app/specification/AppBookSpecification.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.booklore.app.specification; - -import org.booklore.model.entity.*; -import org.booklore.model.enums.BookFileType; -import org.booklore.model.enums.ReadStatus; -import jakarta.persistence.criteria.*; -import org.springframework.data.jpa.domain.Specification; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class AppBookSpecification { - - private AppBookSpecification() { - } - - public static Specification inLibraries(Collection libraryIds) { - return (root, query, cb) -> { - if (libraryIds == null || libraryIds.isEmpty()) { - return cb.conjunction(); - } - return root.get("library").get("id").in(libraryIds); - }; - } - - public static Specification inLibrary(Long libraryId) { - return (root, query, cb) -> { - if (libraryId == null) { - return cb.conjunction(); - } - return cb.equal(root.get("library").get("id"), libraryId); - }; - } - - public static Specification inShelf(Long shelfId) { - return (root, query, cb) -> { - if (shelfId == null) { - return cb.conjunction(); - } - Join shelvesJoin = root.join("shelves", JoinType.INNER); - return cb.equal(shelvesJoin.get("id"), shelfId); - }; - } - - public static Specification withReadStatus(ReadStatus status, Long userId) { - return (root, query, cb) -> { - if (status == null || userId == null) { - return cb.conjunction(); - } - Subquery subquery = query.subquery(Long.class); - Root progressRoot = subquery.from(UserBookProgressEntity.class); - subquery.select(progressRoot.get("book").get("id")) - .where( - cb.equal(progressRoot.get("user").get("id"), userId), - cb.equal(progressRoot.get("readStatus"), status) - ); - return root.get("id").in(subquery); - }; - } - - public static Specification inProgress(Long userId) { - return (root, query, cb) -> { - if (userId == null) { - return cb.conjunction(); - } - Subquery subquery = query.subquery(Long.class); - Root progressRoot = subquery.from(UserBookProgressEntity.class); - subquery.select(progressRoot.get("book").get("id")) - .where( - cb.equal(progressRoot.get("user").get("id"), userId), - progressRoot.get("readStatus").in(ReadStatus.READING, ReadStatus.RE_READING) - ); - return root.get("id").in(subquery); - }; - } - - public static Specification addedWithinDays(int days) { - return (root, query, cb) -> { - Instant cutoff = Instant.now().minus(days, ChronoUnit.DAYS); - return cb.greaterThanOrEqualTo(root.get("addedOn"), cutoff); - }; - } - - public static Specification searchText(String searchQuery) { - return (root, query, cb) -> { - if (searchQuery == null || searchQuery.trim().isEmpty()) { - return cb.conjunction(); - } - String pattern = "%" + searchQuery.toLowerCase().trim() + "%"; - - Join metadataJoin = root.join("metadata", JoinType.LEFT); - Join authorsJoin = metadataJoin.join("authors", JoinType.LEFT); - - List predicates = new ArrayList<>(); - predicates.add(cb.like(cb.lower(metadataJoin.get("title")), pattern)); - predicates.add(cb.like(cb.lower(metadataJoin.get("seriesName")), pattern)); - predicates.add(cb.like(cb.lower(authorsJoin.get("name")), pattern)); - - query.distinct(true); - - return cb.or(predicates.toArray(new Predicate[0])); - }; - } - - public static Specification notDeleted() { - return (root, query, cb) -> cb.or( - cb.isNull(root.get("deleted")), - cb.equal(root.get("deleted"), false) - ); - } - - public static Specification hasScannedOn() { - return (root, query, cb) -> cb.isNotNull(root.get("scannedOn")); - } - - public static Specification hasDigitalFile() { - return (root, query, cb) -> cb.isNotEmpty(root.get("bookFiles")); - } - - public static Specification hasAudiobookFile() { - return (root, query, cb) -> { - Subquery subquery = query.subquery(Long.class); - Root bookFileRoot = subquery.from(BookFileEntity.class); - subquery.select(bookFileRoot.get("book").get("id")) - .where(cb.equal(bookFileRoot.get("bookType"), BookFileType.AUDIOBOOK)); - return root.get("id").in(subquery); - }; - } - - public static Specification hasNonAudiobookFile() { - return (root, query, cb) -> { - Subquery subquery = query.subquery(Long.class); - Root bookFileRoot = subquery.from(BookFileEntity.class); - subquery.select(bookFileRoot.get("book").get("id")) - .where(cb.notEqual(bookFileRoot.get("bookType"), BookFileType.AUDIOBOOK)); - return root.get("id").in(subquery); - }; - } - - /** - * Filter books that have at least one file of the given type. - */ - public static Specification withFileType(BookFileType fileType) { - return (root, query, cb) -> { - if (fileType == null) { - return cb.conjunction(); - } - Subquery subquery = query.subquery(Long.class); - Root bookFileRoot = subquery.from(BookFileEntity.class); - subquery.select(bookFileRoot.get("book").get("id")) - .where(cb.equal(bookFileRoot.get("bookType"), fileType)); - return root.get("id").in(subquery); - }; - } - - /** - * Filter books where the user's personal rating is >= minRating. - */ - public static Specification withMinRating(int minRating, Long userId) { - return (root, query, cb) -> { - Subquery subquery = query.subquery(Long.class); - Root progressRoot = subquery.from(UserBookProgressEntity.class); - subquery.select(progressRoot.get("book").get("id")) - .where( - cb.equal(progressRoot.get("user").get("id"), userId), - cb.greaterThanOrEqualTo(progressRoot.get("personalRating"), minRating) - ); - return root.get("id").in(subquery); - }; - } - - /** - * Filter books where the user's personal rating is <= maxRating. - * Use maxRating=0 to find unrated books. - */ - public static Specification withMaxRating(int maxRating, Long userId) { - return (root, query, cb) -> { - Subquery subquery = query.subquery(Long.class); - Root progressRoot = subquery.from(UserBookProgressEntity.class); - - if (maxRating == 0) { - // Unrated: books with no progress entry or null personalRating - Subquery ratedSubquery = query.subquery(Long.class); - Root ratedRoot = ratedSubquery.from(UserBookProgressEntity.class); - ratedSubquery.select(ratedRoot.get("book").get("id")) - .where( - cb.equal(ratedRoot.get("user").get("id"), userId), - cb.isNotNull(ratedRoot.get("personalRating")) - ); - return cb.not(root.get("id").in(ratedSubquery)); - } - - subquery.select(progressRoot.get("book").get("id")) - .where( - cb.equal(progressRoot.get("user").get("id"), userId), - cb.lessThanOrEqualTo(progressRoot.get("personalRating"), maxRating) - ); - return root.get("id").in(subquery); - }; - } - - /** - * Filter books by author name (case-insensitive exact match). - */ - public static Specification withAuthor(String authorName) { - return (root, query, cb) -> { - if (authorName == null || authorName.trim().isEmpty()) { - return cb.conjunction(); - } - Join metadataJoin = root.join("metadata", JoinType.LEFT); - Join authorsJoin = metadataJoin.join("authors", JoinType.LEFT); - query.distinct(true); - return cb.equal(cb.lower(authorsJoin.get("name")), authorName.toLowerCase().trim()); - }; - } - - /** - * Filter books by language code (case-insensitive). - */ - public static Specification withLanguage(String language) { - return (root, query, cb) -> { - if (language == null || language.trim().isEmpty()) { - return cb.conjunction(); - } - Join metadataJoin = root.join("metadata", JoinType.LEFT); - return cb.equal(cb.lower(metadataJoin.get("language")), language.toLowerCase().trim()); - }; - } - - public static Specification inSeries(String seriesName) { - return (root, query, cb) -> { - if (seriesName == null || seriesName.trim().isEmpty()) { - return cb.conjunction(); - } - Join metadataJoin = root.join("metadata", JoinType.LEFT); - return cb.equal(metadataJoin.get("seriesName"), seriesName); - }; - } - - @SafeVarargs - public static Specification combine(Specification... specs) { - Specification result = (root, query, cb) -> cb.conjunction(); - for (Specification spec : specs) { - if (spec != null) { - result = result.and(spec); - } - } - return result; - } -} diff --git a/booklore-api/src/test/java/org/booklore/app/controller/AppFilterControllerTest.java b/booklore-api/src/test/java/org/booklore/app/controller/AppFilterControllerTest.java deleted file mode 100644 index 20813f85a..000000000 --- a/booklore-api/src/test/java/org/booklore/app/controller/AppFilterControllerTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.AppFilterOptions; -import org.booklore.app.service.AppBookService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.ResponseEntity; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AppFilterControllerTest { - - @Mock - private AppBookService mobileBookService; - - @InjectMocks - private AppFilterController controller; - - @Test - void getFilterOptions_noParams_delegatesWithNulls() { - AppFilterOptions expected = buildOptions(List.of("Author A"), List.of("EPUB"), List.of("en")); - when(mobileBookService.getFilterOptions(null, null, null)).thenReturn(expected); - - ResponseEntity response = controller.getFilterOptions(null, null, null); - - assertEquals(200, response.getStatusCode().value()); - assertSame(expected, response.getBody()); - verify(mobileBookService).getFilterOptions(null, null, null); - } - - @Test - void getFilterOptions_withLibraryId_passesLibraryId() { - AppFilterOptions expected = buildOptions(List.of("Author B"), List.of("PDF"), List.of("fr")); - when(mobileBookService.getFilterOptions(5L, null, null)).thenReturn(expected); - - ResponseEntity response = controller.getFilterOptions(5L, null, null); - - assertEquals(200, response.getStatusCode().value()); - assertEquals(1, response.getBody().getAuthors().size()); - assertEquals("Author B", response.getBody().getAuthors().getFirst().getName()); - verify(mobileBookService).getFilterOptions(5L, null, null); - } - - @Test - void getFilterOptions_withShelfId_passesShelfId() { - AppFilterOptions expected = buildOptions(List.of(), List.of("EPUB"), List.of()); - when(mobileBookService.getFilterOptions(null, 10L, null)).thenReturn(expected); - - ResponseEntity response = controller.getFilterOptions(null, 10L, null); - - assertEquals(200, response.getStatusCode().value()); - assertTrue(response.getBody().getAuthors().isEmpty()); - verify(mobileBookService).getFilterOptions(null, 10L, null); - } - - @Test - void getFilterOptions_withMagicShelfId_passesMagicShelfId() { - AppFilterOptions expected = buildOptions(List.of("Author C"), List.of("MOBI"), List.of("de")); - when(mobileBookService.getFilterOptions(null, null, 7L)).thenReturn(expected); - - ResponseEntity response = controller.getFilterOptions(null, null, 7L); - - assertEquals(200, response.getStatusCode().value()); - assertEquals("Author C", response.getBody().getAuthors().getFirst().getName()); - verify(mobileBookService).getFilterOptions(null, null, 7L); - } - - private AppFilterOptions buildOptions(List authorNames, List fileTypes, List langCodes) { - List authors = authorNames.stream() - .map(name -> AppFilterOptions.AuthorOption.builder().name(name).count(1L).build()) - .toList(); - List languages = langCodes.stream() - .map(code -> AppFilterOptions.LanguageOption.builder().code(code).label(code).count(1L).build()) - .toList(); - return AppFilterOptions.builder() - .authors(authors) - .languages(languages) - .fileTypes(fileTypes) - .readStatuses(List.of("READ", "READING", "UNREAD")) - .build(); - } -} diff --git a/booklore-api/src/test/java/org/booklore/app/controller/AppSeriesControllerTest.java b/booklore-api/src/test/java/org/booklore/app/controller/AppSeriesControllerTest.java deleted file mode 100644 index 68e638401..000000000 --- a/booklore-api/src/test/java/org/booklore/app/controller/AppSeriesControllerTest.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.booklore.app.controller; - -import org.booklore.app.dto.*; -import org.booklore.app.service.AppSeriesService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.ResponseEntity; - -import java.time.Instant; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class AppSeriesControllerTest { - - @Mock - private AppSeriesService mobileSeriesService; - - @InjectMocks - private AppSeriesController controller; - - @Test - void getSeries_defaultParams_delegatesCorrectly() { - AppPageResponse expected = buildSeriesPage(); - when(mobileSeriesService.getSeries(0, 20, "recentlyAdded", "desc", null, null, false)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeries(0, 20, "recentlyAdded", "desc", null, null, null); - - assertEquals(200, response.getStatusCode().value()); - assertSame(expected, response.getBody()); - verify(mobileSeriesService).getSeries(0, 20, "recentlyAdded", "desc", null, null, false); - } - - @Test - void getSeries_withLibraryId_passesLibraryId() { - AppPageResponse expected = buildSeriesPage(); - when(mobileSeriesService.getSeries(0, 20, "recentlyAdded", "desc", 5L, null, false)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeries(0, 20, "recentlyAdded", "desc", 5L, null, null); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeries(0, 20, "recentlyAdded", "desc", 5L, null, false); - } - - @Test - void getSeries_withSearch_passesSearch() { - AppPageResponse expected = buildSeriesPage(); - when(mobileSeriesService.getSeries(0, 20, "recentlyAdded", "desc", null, "harry", false)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeries(0, 20, "recentlyAdded", "desc", null, "harry", null); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeries(0, 20, "recentlyAdded", "desc", null, "harry", false); - } - - @Test - void getSeries_withInProgressStatus_setsInProgressTrue() { - AppPageResponse expected = buildSeriesPage(); - when(mobileSeriesService.getSeries(0, 20, "recentlyAdded", "desc", null, null, true)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeries(0, 20, "recentlyAdded", "desc", null, null, "in-progress"); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeries(0, 20, "recentlyAdded", "desc", null, null, true); - } - - @Test - void getSeries_withUnknownStatus_treatedAsNotInProgress() { - AppPageResponse expected = buildSeriesPage(); - when(mobileSeriesService.getSeries(0, 20, "recentlyAdded", "desc", null, null, false)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeries(0, 20, "recentlyAdded", "desc", null, null, "completed"); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeries(0, 20, "recentlyAdded", "desc", null, null, false); - } - - @Test - void getSeriesBooks_defaultParams_delegatesCorrectly() { - AppPageResponse expected = buildBookPage(); - when(mobileSeriesService.getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", null)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", null); - - assertEquals(200, response.getStatusCode().value()); - assertSame(expected, response.getBody()); - verify(mobileSeriesService).getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", null); - } - - @Test - void getSeriesBooks_withLibraryId_passesLibraryId() { - AppPageResponse expected = buildBookPage(); - when(mobileSeriesService.getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", 5L)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", 5L); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeriesBooks("Dune", 0, 20, "seriesNumber", "asc", 5L); - } - - @Test - void getSeriesBooks_encodedSeriesName_passedAsIs() { - AppPageResponse expected = buildBookPage(); - when(mobileSeriesService.getSeriesBooks("A Song of Ice and Fire", 0, 20, "seriesNumber", "asc", null)) - .thenReturn(expected); - - ResponseEntity> response = - controller.getSeriesBooks("A Song of Ice and Fire", 0, 20, "seriesNumber", "asc", null); - - assertEquals(200, response.getStatusCode().value()); - verify(mobileSeriesService).getSeriesBooks("A Song of Ice and Fire", 0, 20, "seriesNumber", "asc", null); - } - - // ---- Helpers ---- - - private AppPageResponse buildSeriesPage() { - AppSeriesSummary summary = AppSeriesSummary.builder() - .seriesName("Test Series") - .bookCount(5) - .seriesTotal(5) - .booksRead(2) - .authors(List.of("Author A")) - .latestAddedOn(Instant.now()) - .coverBooks(List.of()) - .build(); - return AppPageResponse.of(List.of(summary), 0, 20, 1); - } - - private AppPageResponse buildBookPage() { - AppBookSummary summary = AppBookSummary.builder() - .id(1L) - .title("Test Book") - .authors(List.of("Author A")) - .seriesName("Dune") - .seriesNumber(1.0f) - .build(); - return AppPageResponse.of(List.of(summary), 0, 20, 1); - } -} diff --git a/booklore-api/src/test/java/org/booklore/app/service/AppAuthorServiceTest.java b/booklore-api/src/test/java/org/booklore/app/service/AppAuthorServiceTest.java deleted file mode 100644 index 0ee7ef300..000000000 --- a/booklore-api/src/test/java/org/booklore/app/service/AppAuthorServiceTest.java +++ /dev/null @@ -1,377 +0,0 @@ -package org.booklore.app.service; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.APIException; -import org.booklore.app.dto.AppAuthorDetail; -import org.booklore.app.dto.AppAuthorSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.AuthorEntity; -import org.booklore.repository.AuthorRepository; -import org.booklore.util.FileService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AppAuthorServiceTest { - - @Mock private EntityManager entityManager; - @Mock private AuthenticationService authenticationService; - @Mock private AuthorRepository authorRepository; - @Mock private FileService fileService; - - private AppAuthorService service; - - private final Long userId = 1L; - - @BeforeEach - void setUp() { - service = new AppAuthorService(authorRepository, authenticationService, fileService, entityManager); - } - - // ---- getAuthors tests ---- - - @Nested - class GetAuthorsTests { - - @Test - void getAuthors_admin_noFilters_returnsPage() { - mockAdminUser(); - mockCountQuery(2L); - mockDataQuery(List.of( - new Object[]{buildAuthor(1L, "Author A"), 5L}, - new Object[]{buildAuthor(2L, "Author B"), 3L} - )); - mockAuthorThumbnailExists(1L, true); - mockAuthorThumbnailExists(2L, false); - - AppPageResponse result = service.getAuthors(0, 30, "name", "asc", null, null, null); - - assertNotNull(result); - assertEquals(2, result.getContent().size()); - assertEquals("Author A", result.getContent().get(0).getName()); - assertEquals(5, result.getContent().get(0).getBookCount()); - assertTrue(result.getContent().get(0).isHasPhoto()); - assertEquals("Author B", result.getContent().get(1).getName()); - assertEquals(3, result.getContent().get(1).getBookCount()); - assertFalse(result.getContent().get(1).isHasPhoto()); - assertEquals(2, result.getTotalElements()); - } - - @Test - void getAuthors_admin_emptyResult_returnsEmptyPage() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(0, 30, null, null, null, null, null); - - assertNotNull(result); - assertTrue(result.getContent().isEmpty()); - assertEquals(0, result.getTotalElements()); - } - - @Test - void getAuthors_nonAdmin_withAccessibleLibrary_succeeds() { - mockNonAdminUser(Set.of(5L, 10L)); - mockCountQuery(1L); - mockDataQuery(List.of( - new Object[]{buildAuthor(3L, "Author C"), 2L} - )); - mockAuthorThumbnailExists(3L, false); - - AppPageResponse result = service.getAuthors(0, 30, null, null, 5L, null, null); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals("Author C", result.getContent().get(0).getName()); - } - - @Test - void getAuthors_nonAdmin_noLibraryFilter_usesAccessibleLibraries() { - mockNonAdminUser(Set.of(5L)); - mockCountQuery(1L); - mockDataQuery(List.of( - new Object[]{buildAuthor(4L, "Author D"), 1L} - )); - mockAuthorThumbnailExists(4L, true); - - AppPageResponse result = service.getAuthors(0, 30, null, null, null, null, null); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - - @Test - void getAuthors_withSearch_filtersResultsByName() { - mockAdminUser(); - mockCountQuery(1L); - mockDataQuery(List.of( - new Object[]{buildAuthor(5L, "Brandon Sanderson"), 12L} - )); - mockAuthorThumbnailExists(5L, true); - - AppPageResponse result = service.getAuthors(0, 30, null, null, null, "brandon", null); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals("Brandon Sanderson", result.getContent().get(0).getName()); - } - - @Test - void getAuthors_withHasPhotoTrue_filtersToAuthorsWithPhotos() { - mockAdminUser(); - mockCountQuery(2L); - mockDataQuery(List.of( - new Object[]{buildAuthor(6L, "Author E"), 4L}, - new Object[]{buildAuthor(7L, "Author F"), 2L} - )); - mockAuthorThumbnailExists(6L, true); - mockAuthorThumbnailExists(7L, false); - // Mock the hasPhoto count query - mockAuthorEntityQuery(List.of(buildAuthor(6L, "Author E"), buildAuthor(7L, "Author F"))); - - AppPageResponse result = service.getAuthors(0, 30, null, null, null, null, true); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals("Author E", result.getContent().get(0).getName()); - assertTrue(result.getContent().get(0).isHasPhoto()); - } - - @Test - void getAuthors_withHasPhotoFalse_filtersToAuthorsWithoutPhotos() { - mockAdminUser(); - mockCountQuery(2L); - mockDataQuery(List.of( - new Object[]{buildAuthor(8L, "Author G"), 3L}, - new Object[]{buildAuthor(9L, "Author H"), 1L} - )); - mockAuthorThumbnailExists(8L, true); - mockAuthorThumbnailExists(9L, false); - mockAuthorEntityQuery(List.of(buildAuthor(8L, "Author G"), buildAuthor(9L, "Author H"))); - - AppPageResponse result = service.getAuthors(0, 30, null, null, null, null, false); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals("Author H", result.getContent().get(0).getName()); - assertFalse(result.getContent().get(0).isHasPhoto()); - } - - @Test - void getAuthors_paginationDefaults_appliedCorrectly() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(null, null, null, null, null, null, null); - - assertEquals(0, result.getPage()); - assertEquals(30, result.getSize()); - } - - @Test - void getAuthors_pageSizeCapped_atMax() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(0, 100, null, null, null, null, null); - - assertEquals(50, result.getSize()); - } - - @Test - void getAuthors_sortByName_asc() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(0, 30, "name", "asc", null, null, null); - - assertNotNull(result); - } - - @Test - void getAuthors_sortByBookCount_desc() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(0, 30, "bookCount", "desc", null, null, null); - - assertNotNull(result); - } - - @Test - void getAuthors_sortByRecent_desc() { - mockAdminUser(); - mockCountQuery(0L); - - AppPageResponse result = service.getAuthors(0, 30, "recent", "desc", null, null, null); - - assertNotNull(result); - } - } - - // ---- getAuthorDetail tests ---- - - @Nested - class GetAuthorDetailTests { - - @Test - void getAuthorDetail_admin_returnsDetail() { - mockAdminUser(); - AuthorEntity author = buildAuthor(1L, "J.R.R. Tolkien"); - author.setDescription("English writer and philologist."); - author.setAsin("B000AP9MCS"); - when(authorRepository.findById(1L)).thenReturn(Optional.of(author)); - mockAuthorThumbnailExists(1L, true); - mockBookCountQuery(3); - - AppAuthorDetail result = service.getAuthorDetail(1L); - - assertNotNull(result); - assertEquals(1L, result.getId()); - assertEquals("J.R.R. Tolkien", result.getName()); - assertEquals("English writer and philologist.", result.getDescription()); - assertEquals("B000AP9MCS", result.getAsin()); - assertEquals(3, result.getBookCount()); - assertTrue(result.isHasPhoto()); - } - - @Test - void getAuthorDetail_nonAdmin_withAccess_returnsDetail() { - mockNonAdminUser(Set.of(5L)); - AuthorEntity author = buildAuthor(2L, "Frank Herbert"); - when(authorRepository.findById(2L)).thenReturn(Optional.of(author)); - when(authorRepository.existsByIdAndLibraryIds(eq(2L), anySet())).thenReturn(true); - mockAuthorThumbnailExists(2L, false); - mockBookCountQuery(6); - - AppAuthorDetail result = service.getAuthorDetail(2L); - - assertNotNull(result); - assertEquals("Frank Herbert", result.getName()); - assertEquals(6, result.getBookCount()); - assertFalse(result.isHasPhoto()); - } - - @Test - void getAuthorDetail_nonAdmin_noAccess_throwsNotFound() { - mockNonAdminUser(Set.of(5L)); - AuthorEntity author = buildAuthor(3L, "Secret Author"); - when(authorRepository.findById(3L)).thenReturn(Optional.of(author)); - when(authorRepository.existsByIdAndLibraryIds(eq(3L), anySet())).thenReturn(false); - - assertThrows(APIException.class, () -> service.getAuthorDetail(3L)); - } - - @Test - void getAuthorDetail_nonExistentAuthor_throwsNotFound() { - mockAdminUser(); - when(authorRepository.findById(999L)).thenReturn(Optional.empty()); - - assertThrows(APIException.class, () -> service.getAuthorDetail(999L)); - } - - @Test - void getAuthorDetail_nonAdmin_emptyLibraries_throwsNotFound() { - mockNonAdminUser(Collections.emptySet()); - AuthorEntity author = buildAuthor(4L, "Author X"); - when(authorRepository.findById(4L)).thenReturn(Optional.of(author)); - - assertThrows(APIException.class, () -> service.getAuthorDetail(4L)); - } - } - - // ---- Helpers ---- - - private void mockAdminUser() { - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(true); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - private void mockNonAdminUser(Set libraryIds) { - List assignedLibraries = libraryIds.stream() - .map(id -> Library.builder().id(id).build()) - .toList(); - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(false); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .assignedLibraries(assignedLibraries) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - @SuppressWarnings("unchecked") - private void mockCountQuery(long count) { - TypedQuery countQ = mock(TypedQuery.class); - when(countQ.setParameter(anyString(), any())).thenReturn(countQ); - when(countQ.getSingleResult()).thenReturn(count); - when(entityManager.createQuery(anyString(), eq(Long.class))).thenReturn(countQ); - } - - @SuppressWarnings("unchecked") - private void mockDataQuery(List results) { - TypedQuery dataQ = mock(TypedQuery.class); - when(dataQ.setParameter(anyString(), any())).thenReturn(dataQ); - when(dataQ.setFirstResult(anyInt())).thenReturn(dataQ); - when(dataQ.setMaxResults(anyInt())).thenReturn(dataQ); - when(dataQ.getResultList()).thenReturn(results); - when(entityManager.createQuery(anyString(), eq(Object[].class))).thenReturn(dataQ); - } - - @SuppressWarnings("unchecked") - private void mockAuthorEntityQuery(List authors) { - TypedQuery authorQ = mock(TypedQuery.class); - when(authorQ.setParameter(anyString(), any())).thenReturn(authorQ); - when(authorQ.getResultList()).thenReturn(authors); - when(entityManager.createQuery(anyString(), eq(AuthorEntity.class))).thenReturn(authorQ); - } - - @SuppressWarnings("unchecked") - private void mockBookCountQuery(int count) { - TypedQuery countQ = mock(TypedQuery.class); - when(countQ.setParameter(anyString(), any())).thenReturn(countQ); - when(countQ.getSingleResult()).thenReturn((long) count); - when(entityManager.createQuery(anyString(), eq(Long.class))).thenReturn(countQ); - } - - private void mockAuthorThumbnailExists(Long authorId, boolean exists) { - String path = "/mock/authors/" + authorId + "/thumbnail.jpg"; - when(fileService.getAuthorThumbnailFile(authorId)).thenReturn(path); - // Since Files.exists checks real filesystem, a non-existent mock path returns false. - // For "exists = true" tests, we need a path that actually exists. - if (exists) { - // Use a path we know exists โ€” the temp directory - when(fileService.getAuthorThumbnailFile(authorId)).thenReturn(System.getProperty("java.io.tmpdir")); - } - } - - private AuthorEntity buildAuthor(Long id, String name) { - return AuthorEntity.builder() - .id(id) - .name(name) - .build(); - } -} diff --git a/booklore-api/src/test/java/org/booklore/app/service/AppBookServiceFilterOptionsTest.java b/booklore-api/src/test/java/org/booklore/app/service/AppBookServiceFilterOptionsTest.java deleted file mode 100644 index a217f0ab6..000000000 --- a/booklore-api/src/test/java/org/booklore/app/service/AppBookServiceFilterOptionsTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package org.booklore.app.service; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; -import jakarta.persistence.TypedQuery; -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.APIException; -import org.booklore.app.dto.AppFilterOptions; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.model.dto.Book; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.BookLoreUserEntity; -import org.booklore.model.entity.ShelfEntity; -import org.booklore.model.enums.BookFileType; -import org.booklore.repository.BookRepository; -import org.booklore.repository.ShelfRepository; -import org.booklore.repository.UserBookFileProgressRepository; -import org.booklore.repository.UserBookProgressRepository; -import org.booklore.service.opds.MagicShelfBookService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AppBookServiceFilterOptionsTest { - - @Mock private BookRepository bookRepository; - @Mock private UserBookProgressRepository userBookProgressRepository; - @Mock private UserBookFileProgressRepository userBookFileProgressRepository; - @Mock private ShelfRepository shelfRepository; - @Mock private AuthenticationService authenticationService; - @Mock private AppBookMapper mobileBookMapper; - @Mock private MagicShelfBookService magicShelfBookService; - @Mock private EntityManager entityManager; - - private AppBookService service; - - private final Long userId = 1L; - - @BeforeEach - void setUp() { - service = new AppBookService( - bookRepository, userBookProgressRepository, userBookFileProgressRepository, - shelfRepository, authenticationService, mobileBookMapper, - magicShelfBookService, entityManager - ); - } - - // ------------------------------------------------------------------------- - // Global (no scoping params) - // ------------------------------------------------------------------------- - - @Test - void getFilterOptions_noParams_returnsGlobalOptions() { - mockAdminUser(); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(null, null, null); - - assertNotNull(result); - assertNotNull(result.getAuthors()); - assertNotNull(result.getLanguages()); - assertNotNull(result.getFileTypes()); - assertFalse(result.getReadStatuses().isEmpty()); - } - - // ------------------------------------------------------------------------- - // Library scoping - // ------------------------------------------------------------------------- - - @Test - void getFilterOptions_withLibraryId_admin_succeeds() { - mockAdminUser(); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(5L, null, null); - - assertNotNull(result); - verify(entityManager, times(3)).createQuery(anyString(), any(Class.class)); - } - - @Test - void getFilterOptions_withLibraryId_nonAdminWithAccess_succeeds() { - mockNonAdminUser(Set.of(5L, 10L)); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(5L, null, null); - - assertNotNull(result); - } - - @Test - void getFilterOptions_withLibraryId_nonAdminNoAccess_throwsForbidden() { - mockNonAdminUser(Set.of(10L)); - - assertThrows(APIException.class, () -> service.getFilterOptions(5L, null, null)); - } - - // ------------------------------------------------------------------------- - // Shelf scoping - // ------------------------------------------------------------------------- - - @Test - void getFilterOptions_withShelfId_publicShelf_succeeds() { - mockAdminUser(); - ShelfEntity shelf = ShelfEntity.builder().id(10L).isPublic(true) - .user(BookLoreUserEntity.builder().id(99L).build()).build(); - when(shelfRepository.findById(10L)).thenReturn(Optional.of(shelf)); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(null, 10L, null); - - assertNotNull(result); - } - - @Test - void getFilterOptions_withShelfId_ownPrivateShelf_succeeds() { - mockAdminUser(); - ShelfEntity shelf = ShelfEntity.builder().id(10L).isPublic(false) - .user(BookLoreUserEntity.builder().id(userId).build()).build(); - when(shelfRepository.findById(10L)).thenReturn(Optional.of(shelf)); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(null, 10L, null); - - assertNotNull(result); - } - - @Test - void getFilterOptions_withShelfId_otherPrivateShelf_throwsForbidden() { - mockAdminUser(); - ShelfEntity shelf = ShelfEntity.builder().id(10L).isPublic(false) - .user(BookLoreUserEntity.builder().id(99L).build()).build(); - when(shelfRepository.findById(10L)).thenReturn(Optional.of(shelf)); - - assertThrows(APIException.class, () -> service.getFilterOptions(null, 10L, null)); - } - - @Test - void getFilterOptions_withShelfId_notFound_throwsException() { - mockAdminUser(); - when(shelfRepository.findById(10L)).thenReturn(Optional.empty()); - - assertThrows(APIException.class, () -> service.getFilterOptions(null, 10L, null)); - } - - // ------------------------------------------------------------------------- - // Magic shelf scoping - // ------------------------------------------------------------------------- - - @Test - void getFilterOptions_withMagicShelfId_emptyResult_returnsEmptyOptions() { - mockAdminUser(); - mockMagicShelfBooks(7L, Collections.emptyList()); - - AppFilterOptions result = service.getFilterOptions(null, null, 7L); - - assertNotNull(result); - assertTrue(result.getAuthors().isEmpty()); - assertTrue(result.getLanguages().isEmpty()); - assertTrue(result.getFileTypes().isEmpty()); - assertFalse(result.getReadStatuses().isEmpty()); - } - - @Test - void getFilterOptions_withMagicShelfId_withBooks_returnsFilteredOptions() { - mockAdminUser(); - Book book1 = Book.builder().id(100L).build(); - Book book2 = Book.builder().id(200L).build(); - mockMagicShelfBooks(7L, List.of(book1, book2)); - mockJpqlQueries(); - - AppFilterOptions result = service.getFilterOptions(null, null, 7L); - - assertNotNull(result); - verify(magicShelfBookService).getBooksByMagicShelfId(eq(userId), eq(7L), eq(0), anyInt()); - } - - @Test - void getFilterOptions_withMagicShelfId_serviceThrows_propagatesException() { - mockAdminUser(); - when(magicShelfBookService.getBooksByMagicShelfId(eq(userId), eq(7L), eq(0), anyInt())) - .thenThrow(new RuntimeException("Magic shelf not found")); - - assertThrows(RuntimeException.class, () -> service.getFilterOptions(null, null, 7L)); - } - - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - - private void mockAdminUser() { - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(true); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - private void mockNonAdminUser(Set libraryIds) { - List assignedLibraries = libraryIds.stream() - .map(id -> Library.builder().id(id).build()) - .toList(); - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(false); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .assignedLibraries(assignedLibraries) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - private void mockMagicShelfBooks(Long magicShelfId, List books) { - var page = new PageImpl<>(books, PageRequest.of(0, Math.max(books.size(), 1)), books.size()); - when(magicShelfBookService.getBooksByMagicShelfId(eq(userId), eq(magicShelfId), eq(0), anyInt())) - .thenReturn(page); - } - - @SuppressWarnings("unchecked") - private void mockJpqlQueries() { - TypedQuery authorQuery = mock(TypedQuery.class); - when(authorQuery.setMaxResults(anyInt())).thenReturn(authorQuery); - when(authorQuery.setParameter(anyString(), any())).thenReturn(authorQuery); - when(authorQuery.getResultList()).thenReturn(Collections.emptyList()); - - TypedQuery langQuery = mock(TypedQuery.class); - when(langQuery.setParameter(anyString(), any())).thenReturn(langQuery); - when(langQuery.getResultList()).thenReturn(Collections.emptyList()); - - TypedQuery ftQuery = mock(TypedQuery.class); - when(ftQuery.setParameter(anyString(), any())).thenReturn(ftQuery); - when(ftQuery.getResultList()).thenReturn(Collections.emptyList()); - - when(entityManager.createQuery(anyString(), eq(Tuple.class))) - .thenReturn(authorQuery) - .thenReturn(langQuery); - when(entityManager.createQuery(anyString(), eq(BookFileType.class))) - .thenReturn(ftQuery); - } -} diff --git a/booklore-api/src/test/java/org/booklore/app/service/AppSeriesServiceTest.java b/booklore-api/src/test/java/org/booklore/app/service/AppSeriesServiceTest.java deleted file mode 100644 index c04c2a602..000000000 --- a/booklore-api/src/test/java/org/booklore/app/service/AppSeriesServiceTest.java +++ /dev/null @@ -1,488 +0,0 @@ -package org.booklore.app.service; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.Tuple; -import jakarta.persistence.TypedQuery; -import org.booklore.config.security.service.AuthenticationService; -import org.booklore.exception.APIException; -import org.booklore.app.dto.AppBookSummary; -import org.booklore.app.dto.AppPageResponse; -import org.booklore.app.dto.AppSeriesSummary; -import org.booklore.app.mapper.AppBookMapper; -import org.booklore.model.dto.BookLoreUser; -import org.booklore.model.dto.Library; -import org.booklore.model.entity.*; -import org.booklore.model.enums.BookFileType; -import org.booklore.repository.BookRepository; -import org.booklore.repository.UserBookProgressRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; - -import java.time.Instant; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class AppSeriesServiceTest { - - @Mock private EntityManager entityManager; - @Mock private AuthenticationService authenticationService; - @Mock private BookRepository bookRepository; - @Mock private UserBookProgressRepository userBookProgressRepository; - @Mock private AppBookMapper mobileBookMapper; - - private AppSeriesService service; - - private final Long userId = 1L; - - @BeforeEach - void setUp() { - service = new AppSeriesService( - entityManager, authenticationService, bookRepository, - userBookProgressRepository, mobileBookMapper - ); - } - - // ---- getSeries tests ---- - - @Nested - class GetSeriesTests { - - @Test - void getSeries_admin_noParams_returnsPage() { - mockAdminUser(); - mockAggregateQuery(List.of( - mockSeriesTuple("The Expanse", 9L, 9, Instant.now(), 3L) - )); - mockCountQuery(1L); - mockBooksQuery(List.of(buildBook(1L, "The Expanse", 1.0f, "Author A"))); - - AppPageResponse result = service.getSeries(0, 20, null, null, null, null, false); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals("The Expanse", result.getContent().getFirst().getSeriesName()); - assertEquals(9, result.getContent().getFirst().getBookCount()); - assertEquals(3, result.getContent().getFirst().getBooksRead()); - } - - @Test - void getSeries_admin_emptyResult_returnsEmptyPage() { - mockAdminUser(); - mockAggregateQuery(Collections.emptyList()); - mockCountQuery(0L); - - AppPageResponse result = service.getSeries(0, 20, null, null, null, null, false); - - assertNotNull(result); - assertTrue(result.getContent().isEmpty()); - assertEquals(0, result.getTotalElements()); - } - - @Test - void getSeries_nonAdmin_withAccessibleLibrary_succeeds() { - mockNonAdminUser(Set.of(5L, 10L)); - mockAggregateQuery(List.of( - mockSeriesTuple("Dune", 6L, 6, Instant.now(), 2L) - )); - mockCountQuery(1L); - mockBooksQuery(List.of(buildBook(2L, "Dune", 1.0f, "Frank Herbert"))); - - AppPageResponse result = service.getSeries(0, 20, null, null, 5L, null, false); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - - @Test - void getSeries_nonAdmin_noAccessToLibrary_throwsForbidden() { - mockNonAdminUser(Set.of(10L)); - - assertThrows(APIException.class, () -> - service.getSeries(0, 20, null, null, 5L, null, false)); - } - - @Test - void getSeries_withSearch_passesSearchPattern() { - mockAdminUser(); - mockAggregateQuery(List.of( - mockSeriesTuple("Harry Potter", 7L, 7, Instant.now(), 7L) - )); - mockCountQuery(1L); - mockBooksQuery(List.of(buildBook(3L, "Harry Potter", 1.0f, "J.K. Rowling"))); - - AppPageResponse result = service.getSeries(0, 20, null, null, null, "harry", false); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - - @Test - void getSeries_inProgressOnly_returnsFilteredResults() { - mockAdminUser(); - mockAggregateQueryInProgress(List.of( - mockSeriesTupleInProgress("The Expanse", 9L, 9, Instant.now(), 3L, Instant.now()) - )); - mockCountQueryInProgress(1L); - mockBooksQuery(List.of(buildBook(1L, "The Expanse", 1.0f, "Author A"))); - - AppPageResponse result = service.getSeries(0, 20, null, null, null, null, true); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - - @Test - void getSeries_paginationDefaults_appliedCorrectly() { - mockAdminUser(); - mockAggregateQuery(Collections.emptyList()); - mockCountQuery(0L); - - AppPageResponse result = service.getSeries(null, null, null, null, null, null, false); - - assertEquals(0, result.getPage()); - assertEquals(20, result.getSize()); - } - - @Test - void getSeries_pageSizeCapped_atMax() { - mockAdminUser(); - mockAggregateQuery(Collections.emptyList()); - mockCountQuery(0L); - - AppPageResponse result = service.getSeries(0, 100, null, null, null, null, false); - - assertEquals(50, result.getSize()); - } - - @Test - void getSeries_enrichesCoverBooksAndAuthors() { - mockAdminUser(); - BookEntity book1 = buildBook(10L, "Series X", 1.0f, "Author A"); - BookEntity book2 = buildBook(11L, "Series X", 2.0f, "Author B"); - - mockAggregateQuery(List.of( - mockSeriesTuple("Series X", 2L, 3, Instant.now(), 1L) - )); - mockCountQuery(1L); - mockBooksQuery(List.of(book1, book2)); - - AppPageResponse result = service.getSeries(0, 20, null, null, null, null, false); - - AppSeriesSummary series = result.getContent().getFirst(); - assertEquals(2, series.getCoverBooks().size()); - assertEquals(2, series.getAuthors().size()); - // Cover books should be ordered by seriesNumber ASC - assertEquals(1.0f, series.getCoverBooks().get(0).getSeriesNumber()); - assertEquals(2.0f, series.getCoverBooks().get(1).getSeriesNumber()); - } - - @Test - void getSeries_sortByName_asc() { - mockAdminUser(); - mockAggregateQuery(Collections.emptyList()); - mockCountQuery(0L); - - AppPageResponse result = service.getSeries(0, 20, "name", "asc", null, null, false); - - assertNotNull(result); - } - - @Test - void getSeries_sortByBookCount_desc() { - mockAdminUser(); - mockAggregateQuery(Collections.emptyList()); - mockCountQuery(0L); - - AppPageResponse result = service.getSeries(0, 20, "bookCount", "desc", null, null, false); - - assertNotNull(result); - } - } - - // ---- getSeriesBooks tests ---- - - @Nested - class GetSeriesBooksTests { - - @Test - void getSeriesBooks_admin_returnsBooks() { - mockAdminUser(); - BookEntity book = buildBook(1L, "Dune", 1.0f, "Frank Herbert"); - mockBookPage(List.of(book), 1L); - mockProgress(Collections.emptyList()); - mockMapperSummary(); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 20, null, null, null); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - assertEquals(1, result.getTotalElements()); - } - - @Test - void getSeriesBooks_nonAdmin_withAccess_succeeds() { - mockNonAdminUser(Set.of(5L)); - BookEntity book = buildBook(2L, "Dune", 2.0f, "Frank Herbert"); - mockBookPage(List.of(book), 1L); - mockProgress(Collections.emptyList()); - mockMapperSummary(); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 20, null, null, 5L); - - assertNotNull(result); - assertEquals(1, result.getContent().size()); - } - - @Test - void getSeriesBooks_nonAdmin_noAccess_throwsForbidden() { - mockNonAdminUser(Set.of(10L)); - - assertThrows(APIException.class, () -> - service.getSeriesBooks("Dune", 0, 20, null, null, 5L)); - } - - @Test - void getSeriesBooks_emptyResult_returnsEmptyPage() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Nonexistent", 0, 20, null, null, null); - - assertNotNull(result); - assertTrue(result.getContent().isEmpty()); - assertEquals(0, result.getTotalElements()); - } - - @Test - void getSeriesBooks_sortByTitle_desc() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 20, "title", "desc", null); - - assertNotNull(result); - } - - @Test - void getSeriesBooks_sortByRecentlyAdded_asc() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 20, "recentlyAdded", "asc", null); - - assertNotNull(result); - } - - @Test - void getSeriesBooks_defaultSort_isSeriesNumber() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 20, null, null, null); - - assertNotNull(result); - } - - @Test - void getSeriesBooks_paginationDefaults() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Dune", null, null, null, null, null); - - assertEquals(0, result.getPage()); - assertEquals(20, result.getSize()); - } - - @Test - void getSeriesBooks_pageSizeCapped() { - mockAdminUser(); - mockBookPage(Collections.emptyList(), 0L); - - AppPageResponse result = service.getSeriesBooks("Dune", 0, 200, null, null, null); - - assertEquals(50, result.getSize()); - } - } - - // ---- Helpers ---- - - private void mockAdminUser() { - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(true); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - private void mockNonAdminUser(Set libraryIds) { - List assignedLibraries = libraryIds.stream() - .map(id -> Library.builder().id(id).build()) - .toList(); - var permissions = new BookLoreUser.UserPermissions(); - permissions.setAdmin(false); - BookLoreUser user = BookLoreUser.builder() - .id(userId) - .permissions(permissions) - .assignedLibraries(assignedLibraries) - .build(); - when(authenticationService.getAuthenticatedUser()).thenReturn(user); - } - - private Tuple mockSeriesTuple(String name, Long count, Integer total, Instant addedOn, Long booksRead) { - Tuple tuple = mock(Tuple.class); - when(tuple.get(0, String.class)).thenReturn(name); - when(tuple.get(1, Long.class)).thenReturn(count); - when(tuple.get(2, Integer.class)).thenReturn(total); - when(tuple.get(3, Instant.class)).thenReturn(addedOn); - when(tuple.get(4, Long.class)).thenReturn(booksRead); - return tuple; - } - - private Tuple mockSeriesTupleInProgress(String name, Long count, Integer total, Instant addedOn, Long booksRead, Instant lastReadTime) { - Tuple tuple = mockSeriesTuple(name, count, total, addedOn, booksRead); - when(tuple.get(5, Instant.class)).thenReturn(lastReadTime); - return tuple; - } - - @SuppressWarnings("unchecked") - private void mockAggregateQuery(List results) { - TypedQuery aggregateQ = mock(TypedQuery.class); - when(aggregateQ.setParameter(anyString(), any())).thenReturn(aggregateQ); - when(aggregateQ.setFirstResult(anyInt())).thenReturn(aggregateQ); - when(aggregateQ.setMaxResults(anyInt())).thenReturn(aggregateQ); - when(aggregateQ.getResultList()).thenReturn(results); - - TypedQuery booksQ = mock(TypedQuery.class); - when(booksQ.setParameter(anyString(), any())).thenReturn(booksQ); - when(booksQ.getResultList()).thenReturn(Collections.emptyList()); - - when(entityManager.createQuery(anyString(), eq(Tuple.class))).thenReturn(aggregateQ); - when(entityManager.createQuery(anyString(), eq(BookEntity.class))).thenReturn(booksQ); - } - - @SuppressWarnings("unchecked") - private void mockAggregateQueryInProgress(List results) { - // Pre-compute series names before setting up mocks to avoid calling .get() on mock Tuples during stubbing - List seriesNames = new ArrayList<>(); - for (Tuple t : results) { - seriesNames.add(t.get(0, String.class)); - } - - TypedQuery aggregateQ = mock(TypedQuery.class); - when(aggregateQ.setParameter(anyString(), any())).thenReturn(aggregateQ); - when(aggregateQ.setFirstResult(anyInt())).thenReturn(aggregateQ); - when(aggregateQ.setMaxResults(anyInt())).thenReturn(aggregateQ); - when(aggregateQ.getResultList()).thenReturn(results); - - TypedQuery booksQ = mock(TypedQuery.class); - when(booksQ.setParameter(anyString(), any())).thenReturn(booksQ); - when(booksQ.getResultList()).thenReturn(Collections.emptyList()); - - // In-progress count uses String.class query - TypedQuery countQ = mock(TypedQuery.class); - when(countQ.setParameter(anyString(), any())).thenReturn(countQ); - when(countQ.getResultList()).thenReturn(seriesNames); - - when(entityManager.createQuery(anyString(), eq(Tuple.class))).thenReturn(aggregateQ); - when(entityManager.createQuery(anyString(), eq(BookEntity.class))).thenReturn(booksQ); - when(entityManager.createQuery(anyString(), eq(String.class))).thenReturn(countQ); - } - - @SuppressWarnings("unchecked") - private void mockCountQuery(long count) { - TypedQuery countQ = mock(TypedQuery.class); - when(countQ.setParameter(anyString(), any())).thenReturn(countQ); - when(countQ.getSingleResult()).thenReturn(count); - when(entityManager.createQuery(anyString(), eq(Long.class))).thenReturn(countQ); - } - - @SuppressWarnings("unchecked") - private void mockCountQueryInProgress(long count) { - TypedQuery countQ = mock(TypedQuery.class); - when(countQ.setParameter(anyString(), any())).thenReturn(countQ); - List names = new ArrayList<>(); - for (int i = 0; i < count; i++) names.add("series" + i); - when(countQ.getResultList()).thenReturn(names); - when(entityManager.createQuery(anyString(), eq(String.class))).thenReturn(countQ); - } - - @SuppressWarnings("unchecked") - private void mockBooksQuery(List books) { - TypedQuery booksQ = mock(TypedQuery.class); - when(booksQ.setParameter(anyString(), any())).thenReturn(booksQ); - when(booksQ.getResultList()).thenReturn(books); - when(entityManager.createQuery(anyString(), eq(BookEntity.class))).thenReturn(booksQ); - } - - @SuppressWarnings("unchecked") - private void mockBookPage(List books, long total) { - var page = new PageImpl<>(books, Pageable.ofSize(20), total); - when(bookRepository.findAll(any(Specification.class), any(Pageable.class))).thenReturn(page); - } - - private void mockProgress(List progressList) { - when(userBookProgressRepository.findByUserIdAndBookIdIn(eq(userId), anySet())) - .thenReturn(progressList); - } - - private void mockMapperSummary() { - when(mobileBookMapper.toSummary(any(BookEntity.class), any())) - .thenAnswer(inv -> { - BookEntity b = inv.getArgument(0); - return AppBookSummary.builder() - .id(b.getId()) - .title(b.getMetadata() != null ? b.getMetadata().getTitle() : null) - .build(); - }); - } - - private BookEntity buildBook(Long id, String seriesName, Float seriesNumber, String authorName) { - AuthorEntity author = new AuthorEntity(); - author.setName(authorName); - - BookMetadataEntity metadata = BookMetadataEntity.builder() - .bookId(id) - .title(seriesName + " #" + seriesNumber.intValue()) - .seriesName(seriesName) - .seriesNumber(seriesNumber) - .coverUpdatedOn(Instant.now()) - .authors(List.of(author)) - .build(); - - BookFileEntity bookFile = BookFileEntity.builder() - .id(id) - .bookType(BookFileType.EPUB) - .build(); - - List bookFiles = new ArrayList<>(List.of(bookFile)); - - BookEntity book = BookEntity.builder() - .id(id) - .metadata(metadata) - .addedOn(Instant.now()) - .bookFiles(bookFiles) - .build(); - - metadata.setBook(book); - bookFile.setBook(book); - - return book; - } -} diff --git a/booklore-ui/package-lock.json b/booklore-ui/package-lock.json index 6c9c8d1e1..63c076e4f 100644 --- a/booklore-ui/package-lock.json +++ b/booklore-ui/package-lock.json @@ -293,12 +293,15 @@ } }, "node_modules/@analogjs/vite-plugin-angular": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-2.3.0.tgz", - "integrity": "sha512-3zbx2Pu/PyP0cw5EMgpdVMerYwYOtZvV1pFXxoViGZAxFzQ8TGHCuJm4ECg3jcs7iBdt1O9ZejMuwebVCXeD3A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-2.5.0.tgz", + "integrity": "sha512-TPH74k6qZefHX/RNFqS2D9ZPzR5rFZ6gy5sz63MzQP9iwzRjFbutyBmtnNflTOtQMZIfTLI34ktB9HBzCvvn2g==", "dev": true, "license": "MIT", "dependencies": { + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "oxc-parser": "^0.121.0", "tinyglobby": "^0.2.14", "ts-morph": "^21.0.0" }, @@ -308,7 +311,8 @@ }, "peerDependencies": { "@angular-devkit/build-angular": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0", - "@angular/build": "^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0" + "@angular/build": "^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@angular-devkit/build-angular": { @@ -316,13 +320,16 @@ }, "@angular/build": { "optional": true + }, + "vite": { + "optional": true } } }, "node_modules/@analogjs/vitest-angular": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@analogjs/vitest-angular/-/vitest-angular-2.3.0.tgz", - "integrity": "sha512-nQja8a4a0llFNOdYpI8/7KD2iRPNBuqOUpyPEoa9n7MZPc/50PArShzVVpkOCZ8UYlUiZbkG5ykB23xnUD+iYg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@analogjs/vitest-angular/-/vitest-angular-2.5.0.tgz", + "integrity": "sha512-hOukiUFKu2LiTbYGv8gdpw5UPggidYFuSs/e7+AvZld66eKi0/wSjj43nTLBDEe6oTk6h4CeMyO5QswYwg69Lw==", "dev": true, "license": "MIT", "funding": { @@ -343,13 +350,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.7.tgz", - "integrity": "sha512-4K/5hln9iaPEt3F/NyYqncNLvYpzSjRslEkHl2xIgZwQsIFHEvhnDRBYj2/oatURQhBqO/Yu15z/icVOYLxuTg==", + "version": "0.2102.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.8.tgz", + "integrity": "sha512-b7su7AHIO5F2I6InEu/Bx/oXvGjdCP7kos2tGX73he/lPrTuizooils62OgAzgJ2UeKscyRNUjBPieFCy6XvHQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.7", + "@angular-devkit/core": "21.2.8", "rxjs": "7.8.2" }, "bin": { @@ -362,9 +369,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.7.tgz", - "integrity": "sha512-DONYY5u4IENO2qpd23mODaE4JI2EIohWV1kuJnsU9HIcm5wN714QB2z9WY/s4gLfUiAMIUu/8lpnW/0kOQZAnQ==", + "version": "21.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.8.tgz", + "integrity": "sha512-DyxCILaaic/hfcfiBjAC/SdKE1ybSQIrU62/K5Msn3gZtThZj/T7cG0VHfbmpEFcgYkrQ9caUt6MCg8OoOVDzw==", "dev": true, "license": "MIT", "dependencies": { @@ -390,13 +397,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.7.tgz", - "integrity": "sha512-LYAjjUI1qM7pR/sd0yYt8OLA6ljOOXjcfzV40I5XQNmhAxq90YYS5xwMcixOmWX+z5zvCYGvPXvJGWjzio6SUg==", + "version": "21.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.8.tgz", + "integrity": "sha512-UTEMM1JXzzxufLsTGDsWth2E7+8e9PaFT7nbjUvJ2qevltACkiqAbHEpiD2ISzrSRIO3OirJ+cZtnzXO0FyoBQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.7", + "@angular-devkit/core": "21.2.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.3.0", @@ -408,103 +415,119 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-eslint/builder": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.1.tgz", + "integrity": "sha512-1f1Lyp5e7OH6txiV224HaY3G1uRCj91OSKq7hT2Vw9NRw6zWFc1anBpDeLVjpL9ptUxzUGIQR5jEV54hOPayoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.2.0.tgz", - "integrity": "sha512-J0DWL+j6t9ItFIyIADvzHGqwDA1qfVJ9bx+oTmJ/Hlo7cUpIRoXpcTXpug0CEEABFH0RfDu6PDG2b0FoZ1+7bg==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.1.tgz", + "integrity": "sha512-jjbnJPUXQeQBJ8RM+ahlbt4GH2emVN8JvG3AhFbPci1FrqXi9cOOfkbwLmvpoyTli4LF8gy7g4ctFqnlRgqryw==", "dev": true, "license": "MIT" }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.1.tgz", + "integrity": "sha512-08NNTxwawRLTWPLl8dg1BnXMwimx93y4wMEwx2aWQpJbIt4pmNvwJzd+NgoD/Ag2VdLS/gOMadhJH5fgaYKsPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.3.1", + "@angular-eslint/utils": "21.3.1", + "ts-api-utils": "^2.1.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.1.tgz", + "integrity": "sha512-ndPWJodkcEOu2PVUxlUwyz4D2u3r9KO7veWmStVNOLeNrICJA+nQvrz2BWCu0l48rO0K5ezsy0JFcQDVwE/5mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.3.1", + "@angular-eslint/utils": "21.3.1", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@angular-eslint/template-parser": "21.3.1", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, "node_modules/@angular-eslint/schematics": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.2.0.tgz", - "integrity": "sha512-WtT4fPKIUQ/hswy+l2GF/rKOdD+42L3fUzzcwRzNutQbe2tU9SimoSOAsay/ylWEuhIOQTs7ysPB8fUgFQoLpA==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.1.tgz", + "integrity": "sha512-1U2u4ZsZvwT30aXRLsIJf6tULIiioo9BtASNsldpYecU3/m/1+F61lCYG79qt7YWbif9KABPYZlFTJUFGN8HWA==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.2.0", - "@angular-eslint/eslint-plugin-template": "21.2.0", + "@angular-eslint/eslint-plugin": "21.3.1", + "@angular-eslint/eslint-plugin-template": "21.3.1", "ignore": "7.0.5", - "semver": "7.7.3", + "semver": "7.7.4", "strip-json-comments": "3.1.1" }, "peerDependencies": { "@angular/cli": ">= 21.0.0 < 22.0.0" } }, - "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/eslint-plugin": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.2.0.tgz", - "integrity": "sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0", - "@angular-eslint/utils": "21.2.0", - "ts-api-utils": "^2.1.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.2.0.tgz", - "integrity": "sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0", - "@angular-eslint/utils": "21.2.0", - "aria-query": "5.3.2", - "axobject-query": "4.1.0" - }, - "peerDependencies": { - "@angular-eslint/template-parser": "21.2.0", - "@typescript-eslint/types": "^7.11.0 || ^8.0.0", - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/utils": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz", - "integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, "node_modules/@angular-eslint/template-parser": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.2.0.tgz", - "integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.1.tgz", + "integrity": "sha512-moERVCTekQKOvR8RMuEOtWSO3VS1qrzA3keI1dPto/JVB8Nqp9w3R5ZpEoXHzh4zgEryosxmPgdi6UczJe2ouQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0", - "eslint-scope": "^9.0.0" + "@angular-eslint/bundled-angular-compiler": "21.3.1", + "eslint-scope": "9.1.2" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.1.tgz", + "integrity": "sha512-Q3SGA1/36phZhmsp1mYrKzp/jcmqofRr861MYn46FaWIKSYXBYRzl+H3FIJKBu5CE36Bggu6hbNpwGPuUp+MCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "21.3.1" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular/animations": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.4.tgz", - "integrity": "sha512-hO1P7ks9n7lW3D31bzHohSuoAaj05xJUlK8rZgX8IkH5DLx4qhvfNh0t4bbLuBJLP2r1TaLsQ8KFcemCkFRO2w==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.10.tgz", + "integrity": "sha512-sIzAcxwtRCJ/fu0tK4mo1ooiEaDxJ+Nl6s9nK1D1NP1em12VX03Jx8CMixp/kVtgh4mZnm1x6psBB0FUz3U3Ug==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -513,18 +536,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.4" + "@angular/core": "21.2.10" } }, "node_modules/@angular/build": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.7.tgz", - "integrity": "sha512-FpSkFqpsJtdN1cROekVYkmeV1QepdP+/d7fyYQEuNmlOlyqXSDh9qJmy4iL9VNbAU0rk+vFCtYM86rO7Pt9cSw==", + "version": "21.2.8", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.8.tgz", + "integrity": "sha512-t0PHT7ONDMLwcjC9GaClNF+gsUKN78ofBikw4huiu6np5Rwmxp8KKCrdoRx20lOiibSolXgjZ2Ny0xxjNdNdQA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.7", + "@angular-devkit/architect": "0.2102.8", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -567,7 +590,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.7", + "@angular/ssr": "^21.2.8", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -616,23 +639,27 @@ } } }, - "node_modules/@angular/build/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/@angular/build/node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=10" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/@angular/cdk": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.0.tgz", - "integrity": "sha512-1P0TNL1F51NC7JAaXabaAHY7Y1zBloLSZXfml1POa4a116V+y/QZfPGsxM0LwD1qSSXhSb2LNl7duTtJAP39bA==", + "version": "21.2.9", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.9.tgz", + "integrity": "sha512-0JXsr8f7xjV2815esTSq4+zGqWMa0CyNT/DV1F7lYS6qkYXcFdYUzGcd/WjNL05VKkajkSkWmTi6uyVsOpYdGA==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", @@ -646,19 +673,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.7.tgz", - "integrity": "sha512-N/wj8fFRB718efIFYpwnYfy+MecZREZXsUNMTVndFLH6T0jCheb9PVetR6jsyZp6h46USNPOmJYJ/9255lME+Q==", + "version": "21.2.8", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.8.tgz", + "integrity": "sha512-Y+/US12o+7X2774oeKPsEfHeeYM2SxwnyoXfcaLR8vrMn0zxUrhHebmlz9h83th4EJEuex1Qk0JtF7j5vcwrqQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.7", - "@angular-devkit/core": "21.2.7", - "@angular-devkit/schematics": "21.2.7", + "@angular-devkit/architect": "0.2102.8", + "@angular-devkit/core": "21.2.8", + "@angular-devkit/schematics": "21.2.8", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.7", + "@schematics/angular": "21.2.8", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.48.1", "ini": "6.0.0", @@ -680,23 +707,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/common": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.4.tgz", - "integrity": "sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.10.tgz", + "integrity": "sha512-WLyi/CRLtgALg2mmaqIuKuPnE4i+8PGt/uuz26pVqx+ASh28/TWr5KSCAMomgxEc8kt4OE7lopoQsTihrQCfEw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -705,14 +719,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.4", + "@angular/core": "21.2.10", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.4.tgz", - "integrity": "sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.10.tgz", + "integrity": "sha512-IrgdFuzzD7NTK3WQaSfowjAPxPbnTqsgR92NsOs5ZaWu3RgLl21dHThNc0BK1KwVwppLUSWmD4qePbcLW71VzQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -722,9 +736,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.4.tgz", - "integrity": "sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.10.tgz", + "integrity": "sha512-FDcnj3ogRmnTca4m2GbKP2khFOCtoVvWDZyfw2ZCPAf+zsQlKTyscKvx4GpTFo+KHrYXpawUpDIWHORFpuqFEA==", "dev": true, "license": "MIT", "dependencies": { @@ -745,7 +759,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.4", + "@angular/compiler": "21.2.10", "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { @@ -755,9 +769,9 @@ } }, "node_modules/@angular/core": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.4.tgz", - "integrity": "sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.10.tgz", + "integrity": "sha512-uxH+mbPiCE7rInWKYOPe9Ytas97+mFM6FhFORoN234yBK3b8he+iDuxX6dsbhEFCxhRmfS6hLxe7BdLY6U6kIA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -766,7 +780,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.4", + "@angular/compiler": "21.2.10", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -780,9 +794,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.4.tgz", - "integrity": "sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.10.tgz", + "integrity": "sha512-XOo9qkuBqCLzSBXmyga9ke2tSulxWl+E7Y9Uwqgz8sJtQUlyP/0GYJfu60jiC3NAYobk9K/6h6MsU8zftQKdaA==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -792,16 +806,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.4", - "@angular/core": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/common": "21.2.10", + "@angular/core": "21.2.10", + "@angular/platform-browser": "21.2.10", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.4.tgz", - "integrity": "sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.10.tgz", + "integrity": "sha512-5WMoHGU8BOV3eO9h3vGMIUDPf+3SHis7+X2dHKMtKfFBUtiO8m/lq2x3PzkkKj1782i7KYt92EqPHuADd/eWOw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -810,9 +824,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.4", - "@angular/common": "21.2.4", - "@angular/core": "21.2.4" + "@angular/animations": "21.2.10", + "@angular/common": "21.2.10", + "@angular/core": "21.2.10" }, "peerDependenciesMeta": { "@angular/animations": { @@ -821,9 +835,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.4.tgz", - "integrity": "sha512-LRJLnGh4rdgD0+S5xuDd4YRm5bV8WP2e6F1Pe5rIr6N4V9ofgpB0/uOjYy9se99FJZjoyPnpxaKsp8+XA753Zg==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.10.tgz", + "integrity": "sha512-+/HMJSLnF87EODkHj0AKE3Q8AfYO/8jpTfr731QmplqBtCoLlA/1XR8aYow2hB9YKL9HZWDb2qGkRtCXhrtt+w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -832,16 +846,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.4", - "@angular/compiler": "21.2.4", - "@angular/core": "21.2.4", - "@angular/platform-browser": "21.2.4" + "@angular/common": "21.2.10", + "@angular/compiler": "21.2.10", + "@angular/core": "21.2.10", + "@angular/platform-browser": "21.2.10" } }, "node_modules/@angular/router": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.4.tgz", - "integrity": "sha512-OjWze4XT8i2MThcBXMv7ru1k6/5L6QYZbcXuseqimFCHm2avEJ+mXPovY066fMBZJhqbXdjB82OhHAWkIHjglQ==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.10.tgz", + "integrity": "sha512-4cHHwewIhFEAAaRgJ80371EOtNlydFHbjj/UENLZitjU0azal0mfFCBdkaEdVehd7+mH5xO7MRjy6eFTcTYR5Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -850,16 +864,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.4", - "@angular/core": "21.2.4", - "@angular/platform-browser": "21.2.4", + "@angular/common": "21.2.10", + "@angular/core": "21.2.10", + "@angular/platform-browser": "21.2.10", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "21.2.4", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.2.4.tgz", - "integrity": "sha512-YcPMb0co2hEDwzOG5S27b6f8rotXEUDx88nQuhHDl/ztuzXaxKklJ21qVDVZ0R433YBCRQJl2D6ZrpJojsnBFw==", + "version": "21.2.10", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-21.2.10.tgz", + "integrity": "sha512-rQs3kSmOHOJQoJQ36Y8SXrmHiENpCVgoQ6dwXlJ58Lv7Wm+gdTRrbQD8wyxofgkFpMnD+mf5To3xR86RNNKC2g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -871,32 +885,25 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.4", + "@angular/core": "21.2.10", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", - "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.0.0", - "@csstools/css-color-parser": "^4.0.1", + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.5" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, "engines": { - "node": "20 || >=22" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { @@ -914,15 +921,25 @@ } }, "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/@asamuzakjp/nwsapi": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", @@ -1144,23 +1161,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { @@ -1245,9 +1262,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", - "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -1265,9 +1282,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", "dev": true, "funding": [ { @@ -1289,9 +1306,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", - "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", "dev": true, "funding": [ { @@ -1305,8 +1322,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.1", - "@csstools/css-calc": "^3.0.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" }, "engines": { "node": ">=20.19.0" @@ -1340,9 +1357,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", - "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", "dev": true, "funding": [ { @@ -1354,7 +1371,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -1377,35 +1402,38 @@ } }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1882,15 +1910,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1923,9 +1951,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -1936,7 +1964,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1947,9 +1975,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1981,9 +2009,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -2018,9 +2046,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -2036,14 +2064,11 @@ } }, "node_modules/@gar/promise-retry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", - "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "dev": true, "license": "MIT", - "dependencies": { - "retry": "^0.13.1" - }, "engines": { "node": "^20.17.0 || >=22.9.0" } @@ -2070,29 +2095,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2497,9 +2536,9 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -2557,9 +2596,9 @@ } }, "node_modules/@jsverse/transloco": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.2.1.tgz", - "integrity": "sha512-uuapT1vNi/P9wqklO2VY/sIj8HPVQJ1h+IJFhPbiQvk1FP/vgn2LLwGz/iIcet2bAMJVKKxO8FXytdrwRXXyvg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.3.0.tgz", + "integrity": "sha512-p69/MhhAsTabDAffaiMALx4u9AwAqx24CjdECaCkUKSpCGbiYQUJPCsV4OsWjaSOxDNm9HuIX6NmqbfNZ0S3KA==", "license": "MIT", "dependencies": { "@jsverse/transloco-utils": "^8.2.1", @@ -2572,9 +2611,9 @@ } }, "node_modules/@jsverse/transloco-utils": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.2.1.tgz", - "integrity": "sha512-sAKJQuGgAYRYwndM8X1xVbwOrjENBxKxOwhXE7gFnS8fWUEwBGMswp3wbAOS5jZlLDhyaReesU16ToXLegBCjg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.3.0.tgz", + "integrity": "sha512-HZPDKadKiL3l4iZ51PF7gv7txBgrR7gQB6crdfLSrXsCvCTGDnnhd3y5qO02pBWbWMDKRXKU0clv2dd3jeTSFA==", "license": "MIT", "dependencies": { "cosmiconfig": "^8.1.3", @@ -3160,20 +3199,22 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -3232,9 +3273,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3285,9 +3326,9 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3422,10 +3463,350 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz", + "integrity": "sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.121.0.tgz", + "integrity": "sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.121.0.tgz", + "integrity": "sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.121.0.tgz", + "integrity": "sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.121.0.tgz", + "integrity": "sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.121.0.tgz", + "integrity": "sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.121.0.tgz", + "integrity": "sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.121.0.tgz", + "integrity": "sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.121.0.tgz", + "integrity": "sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.121.0.tgz", + "integrity": "sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.121.0.tgz", + "integrity": "sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.121.0.tgz", + "integrity": "sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.121.0.tgz", + "integrity": "sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.121.0.tgz", + "integrity": "sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.121.0.tgz", + "integrity": "sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.121.0.tgz", + "integrity": "sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.121.0.tgz", + "integrity": "sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.121.0.tgz", + "integrity": "sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.121.0.tgz", + "integrity": "sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.121.0.tgz", + "integrity": "sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxc-project/types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", - "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.121.0.tgz", + "integrity": "sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==", "dev": true, "license": "MIT", "funding": { @@ -4030,9 +4411,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], @@ -4044,9 +4425,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], @@ -4058,9 +4439,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], @@ -4072,9 +4453,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], @@ -4086,9 +4467,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], @@ -4100,9 +4481,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], @@ -4114,9 +4495,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], @@ -4128,9 +4509,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], @@ -4142,9 +4523,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], @@ -4156,9 +4537,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], @@ -4170,9 +4551,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", "cpu": [ "loong64" ], @@ -4184,9 +4565,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", "cpu": [ "loong64" ], @@ -4198,9 +4579,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", "cpu": [ "ppc64" ], @@ -4212,9 +4593,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", "cpu": [ "ppc64" ], @@ -4226,9 +4607,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], @@ -4240,9 +4621,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], @@ -4254,9 +4635,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], @@ -4268,9 +4649,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], @@ -4282,9 +4663,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], @@ -4296,9 +4677,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", "cpu": [ "x64" ], @@ -4310,9 +4691,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], @@ -4324,9 +4705,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], @@ -4338,9 +4719,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], @@ -4352,9 +4733,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -4366,9 +4747,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -4380,14 +4761,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.7", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.7.tgz", - "integrity": "sha512-aqEj3RyBtmH+41HZvrbfrpCo0e+0NzwyQyNSC/wLDShVqoidBtPbEdHU1FZ4+ni41da7rI3F12gUuAHws27kMA==", + "version": "21.2.8", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.8.tgz", + "integrity": "sha512-Kx3PmuZIXhwQqAqoERAXqDCORHFbKTMd+eflXwZfpKkrbWJTVPqKpL4R9RVdEr2E6/VEXDFrdL1whIvGd1xmDg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.7", - "@angular-devkit/schematics": "21.2.7", + "@angular-devkit/core": "21.2.8", + "@angular-devkit/schematics": "21.2.8", "jsonc-parser": "3.3.1" }, "engines": { @@ -4410,9 +4791,9 @@ } }, "node_modules/@sigstore/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz", - "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz", + "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4420,9 +4801,9 @@ } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", - "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4430,27 +4811,27 @@ } }, "node_modules/@sigstore/sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz", - "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@gar/promise-retry": "^1.0.2", "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.1.0", + "@sigstore/core": "^3.2.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.3", - "proc-log": "^6.1.0", - "promise-retry": "^2.0.1" + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/tuf": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz", - "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4604,13 +4985,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", - "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/showdown": { @@ -4628,20 +5009,20 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", + "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/type-utils": "8.59.1", + "@typescript-eslint/utils": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4651,22 +5032,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.59.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", + "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3" }, "engines": { @@ -4678,18 +5059,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", + "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.59.1", + "@typescript-eslint/types": "^8.59.1", "debug": "^4.4.3" }, "engines": { @@ -4700,18 +5081,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", + "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4722,9 +5103,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", + "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", "dev": true, "license": "MIT", "engines": { @@ -4735,21 +5116,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", + "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4760,13 +5141,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", + "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", "dev": true, "license": "MIT", "engines": { @@ -4778,21 +5159,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", + "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.59.1", + "@typescript-eslint/tsconfig-utils": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4802,20 +5183,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", + "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.59.1", + "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4826,17 +5207,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", + "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4874,29 +5255,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", - "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.18", - "ast-v8-to-istanbul": "^0.3.10", + "@vitest/utils": "4.1.5", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", - "magicast": "^0.5.1", + "magicast": "^0.5.2", "obug": "^2.1.1", - "std-env": "^3.10.0", - "tinyrainbow": "^3.0.3" + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.18", - "vitest": "4.0.18" + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4905,31 +5286,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4938,7 +5319,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -4950,26 +5331,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.5", "pathe": "^2.0.3" }, "funding": { @@ -4977,13 +5358,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4992,9 +5374,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, "license": "MIT", "funding": { @@ -5002,19 +5384,27 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -5080,9 +5470,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -5141,112 +5531,29 @@ } }, "node_modules/angular-eslint": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.2.0.tgz", - "integrity": "sha512-pERqqHIMwD34UT0FoHSNTt4V332vHiAzgkY0rgdUaqSamS94IzbF02EfFxygr53UogQQOXhpLbSSDMOyovB3TA==", + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.3.1.tgz", + "integrity": "sha512-VGQWTyuPAEO/AnZuqHxGBJMYSiZ0tbrHx/OgPCRTKHfbrFU4x+zivS84h9UWoDpDtius1RyD+ZReFjTAEWptiA==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/builder": "21.2.0", - "@angular-eslint/eslint-plugin": "21.2.0", - "@angular-eslint/eslint-plugin-template": "21.2.0", - "@angular-eslint/schematics": "21.2.0", - "@angular-eslint/template-parser": "21.2.0", + "@angular-eslint/builder": "21.3.1", + "@angular-eslint/eslint-plugin": "21.3.1", + "@angular-eslint/eslint-plugin-template": "21.3.1", + "@angular-eslint/schematics": "21.3.1", + "@angular-eslint/template-parser": "21.3.1", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "*", "typescript-eslint": "^8.0.0" } }, - "node_modules/angular-eslint/node_modules/@angular-eslint/builder": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz", - "integrity": "sha512-wcp3J9cbrDwSeI/o1D/DSvMQa8zpKjc5WhRGTx33omhWijCfiVNEAiBLWiEx5Sb/dWcoX8yFNWY5jSgFVy9Sjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": ">= 0.2100.0 < 0.2200.0", - "@angular-devkit/core": ">= 21.0.0 < 22.0.0" - }, - "peerDependencies": { - "@angular/cli": ">= 21.0.0 < 22.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/eslint-plugin": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.2.0.tgz", - "integrity": "sha512-X2Qn2viDsjm91CEMxNrxDH3qkKpp6un0C1F1BW2p/m9J4AUVfOcXwWz9UpHFSHTRQ+YlTJbiH1ZwwAPeKhFaxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0", - "@angular-eslint/utils": "21.2.0", - "ts-api-utils": "^2.1.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.2.0.tgz", - "integrity": "sha512-lJ13Dj0DjR6YiceQR0sRbyWzSzOQ6uZPwK9CJUF3wuZjYAUvL1D61zaU9QrVLtf89NVOxv+dYZHDdu3IDeIqbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0", - "@angular-eslint/utils": "21.2.0", - "aria-query": "5.3.2", - "axobject-query": "4.1.0" - }, - "peerDependencies": { - "@angular-eslint/template-parser": "21.2.0", - "@typescript-eslint/types": "^7.11.0 || ^8.0.0", - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/eslint-plugin-template/node_modules/@angular-eslint/utils": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz", - "integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, - "node_modules/angular-eslint/node_modules/@angular-eslint/eslint-plugin/node_modules/@angular-eslint/utils": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.2.0.tgz", - "integrity": "sha512-E19/hkuvHoNFvctBkmEiGWpy2bbC6cgbr3GNVrn2nGtbI4jnwnDFCGHv50I4LBfvj0PA9E6TWe73ejJ5qoMJWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.2.0" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": "*" - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -5277,13 +5584,16 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -5316,9 +5626,9 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -5355,13 +5665,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.24", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz", + "integrity": "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/beasties": { @@ -5454,9 +5767,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -5474,11 +5787,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -5505,9 +5818,9 @@ } }, "node_modules/cacache": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", - "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", "dev": true, "license": "ISC", "dependencies": { @@ -5520,17 +5833,16 @@ "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^5.0.0" + "ssri": "^13.0.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5578,9 +5890,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true, "funding": [ { @@ -5609,13 +5921,17 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -5714,14 +6030,14 @@ } }, "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -5755,6 +6071,19 @@ "node": ">=20" } }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/cliui/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -5835,9 +6164,9 @@ } }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "dev": true, "license": "MIT", "engines": { @@ -5962,14 +6291,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -5989,25 +6318,25 @@ } }, "node_modules/cssstyle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", - "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^4.1.2", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" }, "engines": { "node": ">=20" } }, "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -6136,13 +6465,10 @@ } }, "node_modules/dompurify": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6185,9 +6511,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", "dev": true, "license": "ISC" }, @@ -6281,9 +6607,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -6373,25 +6699,25 @@ } }, "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -6410,7 +6736,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -6433,9 +6759,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6465,9 +6791,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -6481,39 +6807,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint/node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -6679,9 +6972,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", "dev": true, "license": "MIT", "engines": { @@ -6750,9 +7043,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", + "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", "dev": true, "license": "MIT", "dependencies": { @@ -7029,9 +7322,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -7175,9 +7468,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -7188,9 +7481,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.12.15", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", + "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", "dev": true, "license": "MIT", "engines": { @@ -7211,9 +7504,9 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7598,9 +7891,9 @@ } }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "dev": true, "license": "MIT", "funding": { @@ -7804,6 +8097,19 @@ "node": ">=20.0.0" } }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/listr2/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -7935,6 +8241,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/log-update/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -8020,14 +8356,15 @@ } }, "node_modules/make-fetch-happen": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", - "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", + "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", "dev": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", @@ -8053,9 +8390,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -8160,13 +8497,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -8217,11 +8554,11 @@ } }, "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "minipass": "^3.0.0" }, @@ -8342,9 +8679,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", - "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", + "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", "dev": true, "license": "MIT", "optional": true, @@ -8501,21 +8838,21 @@ "optional": true }, "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", + "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { @@ -8568,9 +8905,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", "dev": true, "license": "MIT" }, @@ -8819,6 +9156,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ordered-binary": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", @@ -8827,6 +9177,44 @@ "license": "MIT", "optional": true }, + "node_modules/oxc-parser": { + "version": "0.121.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.121.0.tgz", + "integrity": "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.121.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.121.0", + "@oxc-parser/binding-android-arm64": "0.121.0", + "@oxc-parser/binding-darwin-arm64": "0.121.0", + "@oxc-parser/binding-darwin-x64": "0.121.0", + "@oxc-parser/binding-freebsd-x64": "0.121.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.121.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.121.0", + "@oxc-parser/binding-linux-arm64-musl": "0.121.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.121.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.121.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.121.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.121.0", + "@oxc-parser/binding-linux-x64-gnu": "0.121.0", + "@oxc-parser/binding-linux-x64-musl": "0.121.0", + "@oxc-parser/binding-openharmony-arm64": "0.121.0", + "@oxc-parser/binding-wasm32-wasi": "0.121.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.121.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.121.0", + "@oxc-parser/binding-win32-x64-msvc": "0.121.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8941,12 +9329,12 @@ "license": "MIT" }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -8994,12 +9382,12 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -9060,9 +9448,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -9139,9 +9527,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -9218,9 +9606,9 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.1.tgz", - "integrity": "sha512-ArX5X+psJjL37/5mRzIbeedYOtxDwjLrbBoTTw9hV6+kQt4v8xKy9R1dKuiIFG9OeQTQwzIaJ74OREBAJJmvoA==", + "version": "21.1.6", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.6.tgz", + "integrity": "sha512-kPLoixeMPXIXdQO+RnALRmMfSbPQYAeGx1yTltqkj7XbMkSm9Dr7IxqZZUYjUHemCcr2wyjW82G68WjSMDj66w==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/motion": "^0.0.10", @@ -9263,16 +9651,6 @@ "node": ">=10" } }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9298,9 +9676,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9418,9 +9796,9 @@ } }, "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { @@ -9477,10 +9855,20 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" } }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9494,31 +9882,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, @@ -9644,9 +10032,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -9770,14 +10158,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -9864,22 +10252,35 @@ } }, "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -9892,13 +10293,13 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -9921,6 +10322,16 @@ "node": ">= 14" } }, + "node_modules/socks/node_modules/ip-address": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.1.tgz", + "integrity": "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -10018,16 +10429,16 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/stdin-discarder": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", - "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", "dev": true, "license": "MIT", "engines": { @@ -10038,14 +10449,14 @@ } }, "node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=20" @@ -10055,13 +10466,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -10104,9 +10515,9 @@ "license": "MIT" }, "node_modules/tar": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -10138,9 +10549,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -10148,14 +10559,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -10165,9 +10576,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -10175,22 +10586,22 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", + "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.29" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", + "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", "dev": true, "license": "MIT" }, @@ -10218,9 +10629,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -10244,9 +10655,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -10331,16 +10742,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "version": "8.59.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", + "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" + "@typescript-eslint/eslint-plugin": "8.59.1", + "@typescript-eslint/parser": "8.59.1", + "@typescript-eslint/typescript-estree": "8.59.1", + "@typescript-eslint/utils": "8.59.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10351,13 +10762,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz", - "integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -10365,38 +10776,12 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", - "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/unique-slug": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", - "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10557,31 +10942,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -10597,12 +10982,15 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -10623,6 +11011,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -10631,6 +11025,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -10690,9 +11087,9 @@ } }, "node_modules/whatwg-url": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", - "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { @@ -10772,22 +11169,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -10841,9 +11222,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -10991,13 +11372,13 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "dev": true, "license": "ISC", "peerDependencies": { - "zod": "^3.25 || ^4" + "zod": "^3.25.28 || ^4" } }, "node_modules/zone.js": { From 2b1af054f6067eab66846b91c9b98a8c5b4eaf22 Mon Sep 17 00:00:00 2001 From: Aku Date: Wed, 29 Apr 2026 17:21:23 +0200 Subject: [PATCH 5/5] fix: use correct reader service based on book type (PDF vs CBX) (#3374) --- .../java/org/booklore/service/komga/KomgaService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java b/booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java index 6f26ed169..58227d6cf 100644 --- a/booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java +++ b/booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java @@ -388,11 +388,11 @@ public class KomgaService { // Make sure pages are cached if (isPDF) { - cbxReaderService.getAvailablePages(bookId); - cbxReaderService.streamPageImage(bookId, pageNumber, outputStream); - } else { pdfReaderService.getAvailablePages(bookId); pdfReaderService.streamPageImage(bookId, pageNumber, outputStream); + } else { + cbxReaderService.getAvailablePages(bookId); + cbxReaderService.streamPageImage(bookId, pageNumber, outputStream); } byte[] imageData = outputStream.toByteArray(); @@ -417,4 +417,4 @@ public class KomgaService { return outputStream.toByteArray(); } } -} \ No newline at end of file +}