diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fa06dffc..7395e691 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,37 +2,31 @@ name: Bug report about: Create a report to help us improve title: '' -labels: '' +labels: bug assignees: '' - --- -# ๐Ÿ› Bug Report for Booklore +# ๐Ÿ› Bug Report -Thank you for taking the time to report this bug. Your feedback helps make Booklore better for everyone! - -Let's squash this bug together! ๐Ÿ”จ - ---- - -## ๐Ÿ“ What happened? +## ๐Ÿ“ Description ## ๐Ÿ”„ Steps to Reproduce -1. -2. -3. -4. +1. +2. +3. +4. **Result:** + ## โœ… Expected Behavior -## ๐Ÿ“ธ Screenshots / Error Messages +## ๐Ÿ“ธ Screenshots / Error Messages _(Optional)_ @@ -44,9 +38,10 @@ ## ๐Ÿ’ป Environment - **Installation:** (e.g., Docker, Unraid, Manual) - **Storage Type:** (e.g., Local HDD/SSD, Synology NAS, SMB Share, NFS Mount, S3 Bucket) -## ๐Ÿ“Œ Additional Context - - -## โœจ Possible Solution _(Optional)_ +## ๐Ÿ’ก Possible Solution _(Optional)_ + + +## ๐Ÿ“Œ Additional Context _(Optional)_ + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index cd886513..bfaf52cf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,14 +4,9 @@ title: '' labels: '' assignees: '' - --- -# โœจ Feature Request for Booklore - -Thank you for contributing to Booklore's development. Your suggestions help shape the future of this project. - ---- +# โœจ Feature Request ## ๐Ÿ“ Description @@ -36,5 +31,5 @@ ## ๐ŸŽจ Technical Details _(Optional)_ -## ๐Ÿ“Œ Additional Context +## ๐Ÿ“Œ Additional Context _(Optional)_ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b4b65005 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,50 @@ +version: 2 +updates: + # Backend โ€“ Gradle (Java / Spring) + - package-ecosystem: "gradle" + directory: "/booklore-api" + schedule: + interval: "weekly" + day: "friday" + time: "03:00" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "backend" + commit-message: + prefix: "chore(deps)" + groups: + gradle-dependencies: + patterns: + - "*" + + # Frontend โ€“ npm (Angular) + - package-ecosystem: "npm" + directory: "/booklore-ui" + schedule: + interval: "weekly" + day: "friday" + time: "03:00" + timezone: "UTC" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "frontend" + commit-message: + prefix: "chore(deps)" + groups: + npm-dependencies: + patterns: + - "*" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + labels: + - "dependencies" + - "ci" + commit-message: + prefix: "chore(deps)" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..5e394a91 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +# ๐Ÿš€ Pull Request + +## ๐Ÿ“ Description + + + + +## ๐Ÿ› ๏ธ Changes Implemented + +- + + +## ๐Ÿงช Testing Strategy + + + + +## ๐Ÿ“ธ Visual Changes _(if applicable)_ + + + +## โš ๏ธ Required Pre-Submission Checklist + + +- [ ] Code adheres to project style guidelines and conventions +- [ ] Branch synchronized with latest `develop` branch +- [ ] Automated unit/integration tests added/updated to cover changes +- [ ] All tests pass locally (`./gradlew test` for backend) +- [ ] Manual testing completed in local development environment +- [ ] Flyway migration versioning follows correct sequence _(if database schema modified)_ +- [ ] Documentation pull request submitted to [booklore-docs](https://github.com/booklore-app/booklore-docs) _(required for features or enhancements that introduce user-facing or visual changes)_ + + +## ๐Ÿ’ฌ Additional Context _(optional)_ + diff --git a/.github/scripts/analyze-changes.sh b/.github/scripts/analyze-changes.sh new file mode 100644 index 00000000..1665f224 --- /dev/null +++ b/.github/scripts/analyze-changes.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Exit immediately if a command exits with a non-zero status. +set -e +# Exit if any command in a pipeline fails, not just the last one. +set -o pipefail + +# Define the path where Flyway migration files are stored. +MIGRATION_PATH="booklore-api/src/main/resources/db/migration" + +# Get ALL changes: Added (A), Modified (M), Renamed (R), Copied (C), Deleted (D) +# for SQL files in the migration path between the comparison ref and the current HEAD. +# The output is saved to a temporary file for further processing. +git diff --name-status --diff-filter=AMRCD $COMPARE_REF...HEAD -- "$MIGRATION_PATH/V*.sql" > /tmp/all_changes.txt + +# The check for no changes is now handled in the workflow. +# If this script runs, it's because changes were detected. + +echo "๐Ÿ“ Migration changes detected:" +# Display the detected changes, indented for readability. +cat /tmp/all_changes.txt | sed 's/^/ /' +echo "" + +# Check for deleted files +# Grep for lines starting with 'D' (Deleted). The '|| true' prevents the script from exiting if no matches are found. +DELETED=$(grep "^D" /tmp/all_changes.txt || true) +if [ -n "$DELETED" ]; then + echo "โŒ ERROR: Deleted migration files detected!" + echo "$DELETED" | sed 's/^/ /' + echo "" + echo "Flyway migrations should NEVER be deleted after being applied." + echo "If you need to revert changes, create a new migration." + exit 1 +fi + +# Check for renamed files +# Grep for lines starting with 'R' (Renamed). +RENAMED=$(grep "^R" /tmp/all_changes.txt || true) +if [ -n "$RENAMED" ]; then + echo "โŒ ERROR: Renamed migration files detected!" + echo "$RENAMED" | sed 's/^/ /' + echo "" + echo "Flyway migrations should NEVER be renamed after being applied." + echo "This will cause issues with migration history tracking." + echo "" + echo "๐Ÿ’ก To fix: Revert the rename and create a new migration file instead." + exit 1 +fi + +# Check for modified files +# Grep for lines starting with 'M' (Modified). +MODIFIED=$(grep "^M" /tmp/all_changes.txt || true) +if [ -n "$MODIFIED" ]; then + echo "โŒ ERROR: Modified migration files detected!" + echo "$MODIFIED" | sed 's/^/ /' + echo "" + echo "Flyway migrations should NEVER be modified after being applied." + echo "This will cause checksum validation failures in environments where it has already been applied." + echo "" + echo "๐Ÿ’ก To fix: Revert the changes and create a new migration file instead." + exit 1 +fi + +# Extract ADDED files for conflict checking in a later step. +# We grep for lines starting with 'A' (Added), then use 'cut' to get just the file path. +# 'touch' ensures the file exists even if there are no added files. +grep "^A" /tmp/all_changes.txt | cut -f2- > /tmp/pr_files.txt || touch /tmp/pr_files.txt + +# Set a GitHub Actions output variable to indicate that migration changes were found. +# This is used by the workflow to decide whether to run subsequent steps. +echo "has_changes=true" >> $GITHUB_OUTPUT diff --git a/.github/scripts/check-conflicts.sh b/.github/scripts/check-conflicts.sh new file mode 100644 index 00000000..34c843d5 --- /dev/null +++ b/.github/scripts/check-conflicts.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Exit immediately if a command exits with a non-zero status. +set -e +# Exit if any command in a pipeline fails, not just the last one. +set -o pipefail + +# If there are no new versions to check, exit gracefully. +# This file is created by 'validate-versions.sh'. +# This can happen if a PR has changes, but none are new migration files. +if [ ! -s /tmp/versions_pr_unique.txt ]; then + echo "โ„น๏ธ No new migration versions to check for conflicts." + exit 0 +fi + +# Define the path where Flyway migration files are stored. +MIGRATION_PATH="booklore-api/src/main/resources/db/migration" + +echo "๐Ÿ” Fetching migration files from $COMPARE_REF..." + +# Get ALL existing migration files from the comparison ref (e.g., 'develop' or a tag). +# 'git ls-tree' lists the contents of a tree object. +# The output is piped to grep to filter for only Flyway SQL files. +# '|| touch' ensures the temp file exists even if no files are found. +git ls-tree -r --name-only $COMPARE_REF -- "$MIGRATION_PATH/" 2>/dev/null | \ + grep "V.*\.sql$" > /tmp/base_files.txt || touch /tmp/base_files.txt + +# Handle the case where no migration files exist in the base branch. +if [ ! -s /tmp/base_files.txt ]; then + echo "โš ๏ธ No migration files found in $COMPARE_REF" + echo "This might be the first migration or the path has changed." + echo "" + echo "โœ… Skipping version conflict check." + + PR_COUNT=$(wc -l < /tmp/versions_pr_unique.txt) + echo "" + echo "๐Ÿ“Š Migration Summary:" + echo " - Existing migrations in $COMPARE_REF: 0" + echo " - New migrations in this PR: $PR_COUNT" + exit 0 +fi + +echo "๐Ÿ“‹ Found $(wc -l < /tmp/base_files.txt) migration files in $COMPARE_REF" + +# Extract versions from the base files. +# The loop reads each file path, extracts the version number from the filename, +# and appends it to a temporary file. +> /tmp/versions_base.txt +while IFS= read -r file; do + filename=$(basename "$file") + # sed extracts the version number (e.g., 1.0.0) from a filename like 'V1.0.0__description.sql'. + version=$(echo "$filename" | sed -n 's/^V\([0-9.]*\)__.*/\1/p') + [ -n "$version" ] && echo "$version" >> /tmp/versions_base.txt +done < /tmp/base_files.txt + +# Create a file with only unique, sorted version numbers from the base. +sort -u /tmp/versions_base.txt > /tmp/versions_base_unique.txt + +BASE_COUNT=$(wc -l < /tmp/versions_base_unique.txt) +echo "๐Ÿ“Š Found $BASE_COUNT unique versions in $COMPARE_REF" + +# Find conflicts between base versions and versions from NEW PR files. +# 'comm -12' finds lines common to both sorted files. +CONFLICTS=$(comm -12 /tmp/versions_base_unique.txt /tmp/versions_pr_unique.txt) + +# If conflicts are found, report them and exit with an error. +if [ -n "$CONFLICTS" ]; then + echo "โŒ Version conflicts detected!" + echo "" + echo "The following versions from your new migration files already exist in $COMPARE_REF:" + echo "$CONFLICTS" | sed 's/^/ V/' + echo "" + + # Show which files have conflicting versions for easier debugging. + echo "Conflicting files:" + while IFS= read -r version; do + echo " Version V$version exists in:" + grep "V${version}__" /tmp/base_files.txt | xargs -n1 basename | sed 's/^/ BASE: /' + # /tmp/pr_files.txt contains only added files from the PR (from analyze-changes.sh). + grep "V${version}__" /tmp/pr_files.txt | xargs -n1 basename | sed 's/^/ PR: /' + done <<< "$CONFLICTS" + + echo "" + echo "๐Ÿ’ก To fix: Use a version number that doesn't exist in $COMPARE_REF" + exit 1 +fi + +echo "โœ… No version conflicts detected." + +# Get the count of new migrations in the PR. +PR_COUNT=$(wc -l < /tmp/versions_pr_unique.txt) + +# Print a final summary. +echo "" +echo "๐Ÿ“Š Migration Summary:" +echo " - Existing migrations in $COMPARE_REF: $BASE_COUNT" +echo " - New migrations in this PR: $PR_COUNT" diff --git a/.github/scripts/determine-compare-ref.sh b/.github/scripts/determine-compare-ref.sh new file mode 100644 index 00000000..13f8839e --- /dev/null +++ b/.github/scripts/determine-compare-ref.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Exit immediately if a command exits with a non-zero status. +set -e +# Exit if any command in a pipeline fails, not just the last one. +set -o pipefail + +# The target branch of the pull request (e.g., 'develop', 'master') is passed as the first argument. +TARGET_BRANCH="$1" +echo "๐ŸŽฏ Target branch: $TARGET_BRANCH" + +# Handle cases where the target branch is not specified, such as a direct push to a branch. +if [ -z "$TARGET_BRANCH" ]; then + echo "โš ๏ธ No target branch specified (e.g., a direct push event). Defaulting to compare with 'develop'." + TARGET_BRANCH="develop" +fi + +# Logic to determine the comparison reference based on the target branch. +if [ "$TARGET_BRANCH" = "master" ]; then + # For PRs to 'master', we compare against the latest git tag. + # This is common for release workflows where 'master' only contains tagged releases. + if ! LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null); then + echo "โš ๏ธ No tags found in repository. Skipping conflict check." + # Set output to signal the workflow to stop. + echo "has_ref=false" >> $GITHUB_OUTPUT + exit 0 + fi + echo "๐Ÿ“Œ Comparing against last tag: $LAST_TAG" + # Set the COMPARE_REF environment variable for subsequent steps in the job. + echo "COMPARE_REF=$LAST_TAG" >> $GITHUB_ENV +else + # For all other cases (PRs to 'develop', other feature branches, or direct pushes), + # we compare against the 'develop' branch. + echo "๐Ÿ”„ Comparing against head of develop branch" + # Ensure the local 'develop' branch is up-to-date with the remote. + git fetch origin develop:develop + # Set the COMPARE_REF to the remote develop branch. + echo "COMPARE_REF=origin/develop" >> $GITHUB_ENV +fi + +# Set a GitHub Actions output variable to indicate that a valid comparison ref was found. +echo "has_ref=true" >> $GITHUB_OUTPUT diff --git a/.github/scripts/validate-versions.sh b/.github/scripts/validate-versions.sh new file mode 100644 index 00000000..f5396811 --- /dev/null +++ b/.github/scripts/validate-versions.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Exit immediately if a command exits with a non-zero status. +set -e +# Exit if any command in a pipeline fails, not just the last one. +set -o pipefail + +# Define the path where Flyway migration files are stored. +MIGRATION_PATH="booklore-api/src/main/resources/db/migration" + +# --- Part 1: Check for duplicate versions within the PR branch itself --- + +# Get ALL migration files in the current HEAD of the PR branch for an internal duplicate check. +find "$MIGRATION_PATH" -type f -name "V*.sql" > /tmp/all_pr_files.txt + +# Check for duplicate versions within the PR branch. This prevents merging a branch +# that contains multiple files with the same version number. +echo "๐Ÿ”Ž Checking for duplicate versions in the branch..." +> /tmp/versions_all_pr.txt +# Loop through all found migration files and extract their version numbers. +while IFS= read -r file; do + filename=$(basename "$file") + # sed extracts the version number (e.g., 1.0.0) from a filename like 'V1.0.0__description.sql'. + version=$(echo "$filename" | sed -n 's/^V\([0-9.]*\)__.*/\1/p') + [ -n "$version" ] && echo "$version" >> /tmp/versions_all_pr.txt +done < /tmp/all_pr_files.txt + +# 'uniq -d' filters for lines that appear more than once in the sorted list. +sort /tmp/versions_all_pr.txt | uniq -d > /tmp/duplicates_in_pr.txt + +# If the duplicates file is not empty, report the error and exit. +if [ -s /tmp/duplicates_in_pr.txt ]; then + echo "โŒ Duplicate migration versions found within the branch!" + echo "" + echo "The following versions are duplicated:" + while IFS= read -r version; do + echo " - Version V$version is used by:" + # Show the conflicting files for easy debugging. + grep "V${version}__" /tmp/all_pr_files.txt | xargs -n1 basename | sed 's/^/ /' + done < /tmp/duplicates_in_pr.txt + echo "" + echo "๐Ÿ’ก To fix: Ensure all migration files have a unique version number." + exit 1 +fi + +echo "โœ… No duplicate versions found within the branch." + +# --- Part 2: Extract versions from NEWLY ADDED files for conflict checking against the base branch --- + +# /tmp/pr_files.txt is created by analyze-changes.sh and contains only ADDED files. +# If the file doesn't exist or is empty, there's nothing to check. +if [ ! -f /tmp/pr_files.txt ] || [ ! -s /tmp/pr_files.txt ]; then + echo "โ„น๏ธ No new migration files to check for conflicts." + # Set output to signal the workflow to skip the conflict check step. + echo "has_versions=false" >> $GITHUB_OUTPUT + exit 0 +fi + +echo "๐Ÿ”Ž Extracting versions from new files..." +> /tmp/versions_pr.txt +# Loop through only the NEWLY ADDED files and extract their versions. +while IFS= read -r file; do + filename=$(basename "$file") + version=$(echo "$filename" | sed -n 's/^V\([0-9.]*\)__.*/\1/p') + [ -n "$version" ] && echo "$version" >> /tmp/versions_pr.txt +done < /tmp/pr_files.txt + +# If no valid versions were extracted from the new files, exit. +if [ ! -s /tmp/versions_pr.txt ]; then + echo "โ„น๏ธ No versions found in new migration files." + echo "has_versions=false" >> $GITHUB_OUTPUT + exit 0 +fi + +# Create a sorted, unique list of versions from the new files. +# This file will be used by 'check-conflicts.sh'. +sort -u /tmp/versions_pr.txt > /tmp/versions_pr_unique.txt + +# Set output to signal that there are new versions to check for conflicts. +echo "has_versions=true" >> $GITHUB_OUTPUT diff --git a/.github/workflows/docker-build-publish.yml b/.github/workflows/docker-build-publish.yml index b046d6b9..d6a84c78 100644 --- a/.github/workflows/docker-build-publish.yml +++ b/.github/workflows/docker-build-publish.yml @@ -2,11 +2,94 @@ name: Build, Tag, Push, and Release to GitHub Container Registry on: push: + branches: + - 'master' + - 'develop' + pull_request: branches: - '**' jobs: + check-for-migrations: + name: Check for DB Migrations + if: github.event_name == 'pull_request' && ((github.base_ref == 'master' && github.head_ref == 'develop') || github.base_ref == 'develop') + runs-on: ubuntu-latest + outputs: + has_migrations: ${{ steps.check_migrations.outputs.has_migrations }} + steps: + - name: Checkout Repository for Diff + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Detect Flyway Migration Changes + id: check_migrations + run: | + # Compare PR head with the target base branch + if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q "booklore-api/src/main/resources/db/migration/V.*.sql"; then + echo "Migration file changes detected. Proceeding with migration preview." + echo "has_migrations=true" >> $GITHUB_OUTPUT + else + echo "No migration file changes detected. Skipping migration preview." + echo "has_migrations=false" >> $GITHUB_OUTPUT + fi + + flyway-migration-preview: + name: Flyway DB Migration Preview + needs: [check-for-migrations] + if: needs.check-for-migrations.outputs.has_migrations == 'true' + runs-on: ubuntu-latest + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: booklore_test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + steps: + - name: Checkout Base Branch + uses: actions/checkout@v6 + with: + ref: '${{ github.base_ref }}' + + - name: Apply Migrations from Base Branch + run: | + echo "Applying migrations from '${{ github.base_ref }}' branch..." + docker run --network host \ + -v ${{ github.workspace }}:/flyway/sql \ + flyway/flyway:11.19.0-alpine \ + -url=jdbc:mariadb://127.0.0.1:3306/booklore_test \ + -user=root -password=root \ + -locations=filesystem:/flyway/sql/booklore-api/src/main/resources/db/migration \ + migrate + + - name: Checkout Pull Request Branch + uses: actions/checkout@v6 + + - name: Apply Migrations from PR Branch + run: | + echo "Applying new migrations from PR branch..." + docker run --network host \ + -v ${{ github.workspace }}:/flyway/sql \ + flyway/flyway:11.19.0-alpine \ + -url=jdbc:mariadb://127.0.0.1:3306/booklore_test \ + -user=root -password=root \ + -locations=filesystem:/flyway/sql/booklore-api/src/main/resources/db/migration \ + migrate + + - name: Confirm Flyway Dry Run Success + run: echo "โœ… Flyway migration preview successful. Migrations can be applied cleanly." + build-and-push: + needs: [check-for-migrations, flyway-migration-preview] + if: always() && (needs.flyway-migration-preview.result == 'success' || needs.flyway-migration-preview.result == 'skipped') runs-on: ubuntu-latest permissions: @@ -18,44 +101,47 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Log in to Docker Hub - uses: docker/login-action@v2 + - name: Authenticate to Docker Hub + if: github.event_name == 'push' + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Log in to GitHub Container Registry - uses: docker/login-action@v2 + - name: Authenticate to GitHub Container Registry + if: github.event_name == 'push' + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - - name: Set up QEMU for multi-arch builds + - name: Set Up QEMU for Multi-Architecture Builds uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx + - name: Set Up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set Up JDK 21 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' + cache: 'gradle' - - name: Run Backend Tests + - name: Execute Backend Tests id: backend_tests working-directory: ./booklore-api run: | echo "Running backend tests with testcontainers..." - ./gradlew test + ./gradlew test --no-daemon --parallel --build-cache continue-on-error: true - - name: Publish Test Results + - name: Publish Backend Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: @@ -65,8 +151,8 @@ jobs: report_individual_runs: true report_suite_logs: 'any' - - name: Upload Test Reports - uses: actions/upload-artifact@v4 + - name: Upload Backend Test Reports + uses: actions/upload-artifact@v6 if: always() with: name: test-reports @@ -75,13 +161,13 @@ jobs: booklore-api/build/test-results/ retention-days: 30 - - name: Check Test Results + - name: Validate Backend Test Results if: steps.backend_tests.outcome == 'failure' run: | echo "โŒ Backend tests failed! Check the test results above." exit 1 - - name: Get Latest Master Version + - name: Retrieve Latest Master Version Tag id: get_version run: | latest_tag=$(git tag --list "v*" --sort=-v:refname | head -n 1) @@ -89,7 +175,7 @@ jobs: echo "latest_tag=$latest_tag" >> $GITHUB_ENV echo "Latest master tag: $latest_tag" - - name: Determine Version Bump (Only for Master) + - name: Determine Version Bump (Master Only) if: github.ref == 'refs/heads/master' id: determine_bump env: @@ -107,11 +193,11 @@ jobs: labels=$(gh pr view "$pr_number" --json labels --jq '.labels[].name' || echo "") echo "PR labels: $labels" - if echo "$labels" | grep -q 'major'; then + if echo "$labels" | grep -q 'bump:major'; then bump="major" - elif echo "$labels" | grep -q 'minor'; then + elif echo "$labels" | grep -q 'bump:minor'; then bump="minor" - elif echo "$labels" | grep -q 'patch'; then + elif echo "$labels" | grep -q 'bump:patch'; then bump="patch" else last_commit_msg=$(git log -1 --pretty=%B) @@ -161,28 +247,44 @@ jobs: echo "image_tag=$image_tag" >> $GITHUB_ENV echo "Image tag: $image_tag" - - name: Build and Push Docker Image - run: | - docker buildx create --use - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg APP_VERSION=${{ env.image_tag }} \ - --build-arg APP_REVISION=${{ github.sha }} \ - --tag booklore/booklore:${{ env.image_tag }} \ - --tag ghcr.io/booklore-app/booklore:${{ env.image_tag }} \ - --push . + - name: Build and push Docker image + if: github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + booklore/booklore:${{ env.image_tag }} + ghcr.io/booklore-app/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 + cache-to: | + type=gha,mode=max + type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max - - name: Push Latest Tag (Only for Master) - if: github.ref == 'refs/heads/master' - run: | - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg APP_VERSION=${{ env.new_tag }} \ - --tag booklore/booklore:latest \ - --tag ghcr.io/booklore-app/booklore:latest \ - --push . + - name: Push Latest Tag (Master Only) + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + booklore/booklore:latest + booklore/booklore:${{ env.new_tag }} + ghcr.io/booklore-app/booklore:latest + ghcr.io/booklore-app/booklore:${{ env.new_tag }} + build-args: | + APP_VERSION=${{ env.new_tag }} + APP_REVISION=${{ github.sha }} + cache-from: type=gha - - name: Update Release Draft (Only for Master) + - name: Update GitHub Release Draft (Master Only) if: github.ref == 'refs/heads/master' uses: release-drafter/release-drafter@v6 with: @@ -191,47 +293,9 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} - - name: Publish Draft Release (Only for Master) + - name: Publish GitHub Draft Release (Master Only) if: github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ github.token }} run: | - gh release edit ${{ env.new_tag }} --draft=false - - - name: Notify Discord of New Release - if: false - continue-on-error: true - shell: bash - env: - GH_TOKEN: ${{ github.token }} - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - NEW_TAG: ${{ env.new_tag }} - run: | - set -euo pipefail - if [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then - echo "DISCORD_WEBHOOK_URL not set, skipping Discord notification." - exit 0 - fi - release_json=$(gh release view "$NEW_TAG" --json name,body,url) - release_name=$(jq -r '.name' <<< "$release_json") - release_body=$(jq -r '.body' <<< "$release_json") - release_url=$(jq -r '.url' <<< "$release_json") - clean_body=$(echo "$release_body" | tr -d '\r') - max_length=1800 - if [ ${#clean_body} -gt $max_length ]; then - clean_body="${clean_body:0:$((max_length-12))}โ€ฆ [truncated]" - fi - payload=$(jq -n \ - --arg title "New Release: $release_name" \ - --arg url "$release_url" \ - --arg desc "$clean_body" \ - '{ - content: null, - embeds: [{ - title: $title, - url: $url, - description: $desc, - color: 3066993 - }] - }') - curl -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" + gh release edit ${{ env.new_tag }} --draft=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40f7ad7f..a3cb080d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,17 @@ # Contributing to Booklore -๐ŸŽ‰ Thanks for your interest in contributing to **Booklore**, a modern, self-hostable digital library system for books and comics. Whether you're fixing bugs, adding features, improving documentation, or asking questions - your contribution matters! +๐ŸŽ‰ **Thank you for your interest in contributing to Booklore!** Whether you're fixing bugs, adding features, improving documentation, or simply asking questions, every contribution helps make Booklore better for everyone. --- -## ๐Ÿ“š Overview +## ๐Ÿ“š What is Booklore? -**Booklore** is a self-hostable platform designed to manage and read books and comics. It includes: +**Booklore** is a modern, self-hostable digital library platform for managing and reading books and comics. It's designed with privacy, flexibility, and ease of use in mind. -- **Frontend**: Angular 20, TypeScript, PrimeNG 19, Tailwind CSS +**Tech Stack:** +- **Frontend**: Angular 20, TypeScript, PrimeNG 19 - **Backend**: Java 21, Spring Boot 3.5 -- **Authentication**: Local JWT + optional OIDC (e.g. Authentik) +- **Authentication**: Local JWT + optional OIDC (e.g., Authentik) - **Database**: MariaDB - **Deployment**: Docker-compatible, reverse proxy-ready @@ -20,116 +21,202 @@ ## ๐Ÿ“ฆ Project Structure ``` booklore/ -โ”œโ”€โ”€ booklore-ui/ # Angular frontend -โ”œโ”€โ”€ booklore-api/ # Spring Boot backend -โ”œโ”€โ”€ assets/ # Shared assets +โ”œโ”€โ”€ booklore-ui/ # Angular frontend application +โ”œโ”€โ”€ booklore-api/ # Spring Boot backend API +โ”œโ”€โ”€ assets/ # Shared assets (logos, icons, etc.) +โ”œโ”€โ”€ docker-compose.yml # Production Docker setup +โ””โ”€โ”€ dev.docker-compose.yml # Development Docker setup ``` --- ## ๐Ÿš€ Getting Started -1. **Fork the repository** on GitHub -2. **Clone your fork** locally: +### 1. Fork and Clone + +First, fork the repository to your GitHub account, then clone it locally: ```bash -git clone https://github.com/adityachandelgit/BookLore.git +# Clone your fork +git clone https://github.com//booklore.git cd booklore + +# Add upstream remote to keep your fork in sync +git remote add upstream https://github.com/booklore-app/booklore.git +``` + +### 2. Keep Your Fork Updated + +Before starting work on a new feature or fix: + +```bash +# Fetch latest changes from upstream +git fetch upstream + +# Merge upstream changes into your local main branch +git checkout main +git merge upstream/main + +# Push updates to your fork +git push origin main ``` --- ## ๐Ÿงฑ Local Development Setup -Booklore has a simple all-in-one Docker development stack, or you can install & run everything manually. +Booklore offers two development approaches: an all-in-one Docker stack for quick setup, or manual installation for more control. +### Option 1: Docker Development Stack (Recommended for Quick Start) -### Development using Docker stack - -Run `docker compose -f dev.docker-compose.yml up` - -- Dev web server is accessible at `http://localhost:4200/` -- Dev database is accessible at `http://localhost:3366/` -- Remote java debugging is accessible at `http://localhost:5005/` - -All ports are configurable using environment variables - see dev.docker-compose.yml - ---- - -### Development on local machine - -#### 1. Prerequisites - -- **Java 21+** -- **Node.js 18+** -- **MariaDB** -- **Docker and Docker Compose** - ---- - -#### 2. Frontend Setup - -To set up the Angular frontend: +This option sets up everything with a single command: ```bash -cd booklore-ui -npm install -ng serve +docker compose -f dev.docker-compose.yml up ``` -The dev server runs at `http://localhost:4200/`. +**What you get:** +- โœ… Frontend dev server at `http://localhost:4200/` +- โœ… Backend API at `http://localhost:8080/` +- โœ… MariaDB at `localhost:3366` +- โœ… Remote Java debugging at `localhost:5005` -> โš ๏ธ Use `--force` with `npm install` only as a last resort for dependency conflicts. +**Note:** All ports are configurable via environment variables in `dev.docker-compose.yml`: +- `FRONTEND_PORT` (default: 4200) +- `BACKEND_PORT` (default: 8080) +- `DB_PORT` (default: 3366) +- `REMOTE_DEBUG_PORT` (default: 5005) + +**Stopping the stack:** +```bash +docker compose -f dev.docker-compose.yml down +``` --- -#### 3. Backend Setup +### Option 2: Manual Local Development -##### a. Configure `application-dev.yml` +For more control over your development environment, you can run each component separately. -Create or edit `booklore-api/src/main/resources/application-dev.yml`: +#### Prerequisites + +Ensure you have the following installed: +- **Java 21+** ([Download](https://adoptium.net/)) +- **Node.js 18+** and **npm** ([Download](https://nodejs.org/)) +- **MariaDB 10.6+** ([Download](https://mariadb.org/download/)) +- **Git** ([Download](https://git-scm.com/)) + +#### Frontend Setup + +```bash +# Navigate to the frontend directory +cd booklore-ui + +# Install dependencies +npm install + +# Start the development server +ng serve + +# Or use npm script +npm start +``` + +The frontend will be available at `http://localhost:4200/` with hot-reload enabled. + +**Common Issues:** +- If you encounter dependency conflicts, try `npm install --legacy-peer-deps` +- Use `--force` only as a last resort + +--- + +#### Backend Setup + +##### Step 1: Configure Application Properties + +Create a development configuration file at `booklore-api/src/main/resources/application-dev.yml`: ```yaml app: - path-book: '/path/to/booklore/books' # Directory for book/comic files - path-config: '/path/to/booklore/config' # Directory for thumbnails, metadata, etc. + # Path where books and comics are stored + path-book: '/Users/yourname/booklore-data/books' + + # Path for thumbnails, metadata cache, and other config files + path-config: '/Users/yourname/booklore-data/config' spring: datasource: driver-class-name: org.mariadb.jdbc.Driver - url: jdbc:mariadb://localhost:3333/booklore?createDatabaseIfNotExist=true + url: jdbc:mariadb://localhost:3306/booklore?createDatabaseIfNotExist=true username: root - password: Password123 + password: your_secure_password ``` -> ๐Ÿ”ง Replace `/path/to/...` with actual local paths +**Important:** +- Replace `/Users/yourname/...` with actual paths on your system +- Create these directories if they don't exist +- Ensure proper read/write permissions -##### b. Run the Backend +**Example paths:** +- **macOS/Linux**: `/Users/yourname/booklore-data/books` +- **Windows**: `C:\Users\yourname\booklore-data\books` + +##### Step 2: Set Up the Database + +Ensure MariaDB is running and create the database: + +```bash +# Connect to MariaDB +mysql -u root -p + +# Create database and user (optional) +CREATE DATABASE IF NOT EXISTS booklore; +CREATE USER 'booklore_user'@'localhost' IDENTIFIED BY 'your_secure_password'; +GRANT ALL PRIVILEGES ON booklore.* TO 'booklore_user'@'localhost'; +FLUSH PRIVILEGES; +EXIT; +``` + +##### Step 3: Run the Backend ```bash cd booklore-api -./gradlew bootRun +./gradlew bootRun --args='--spring.profiles.active=dev' +``` + +The backend API will be available at `http://localhost:8080/` + +**Verify it's running:** +```bash +curl http://localhost:8080/actuator/health ``` --- ## ๐Ÿงช Testing -### Frontend +Always run tests before submitting a pull request to ensure your changes don't break existing functionality. -Run unit tests using: - -```bash -cd booklore-ui -ng test -``` - -### Backend - -Run backend tests using: +### Backend Tests ```bash cd booklore-api + +# Run all tests +./gradlew test + +# Run tests with detailed output +./gradlew test --info + +# Run a specific test class +./gradlew test --tests "com.booklore.api.service.BookServiceTest" + +# Generate coverage report +./gradlew test jacocoTestReport +``` + +**Before creating a PR, always run:** +```bash ./gradlew test ``` @@ -137,74 +224,199 @@ ### Backend ## ๐Ÿ› ๏ธ Contributing Guidelines -### ๐Ÿ’ก Bug Reports +### ๐Ÿ’ก Reporting Bugs -- Check [existing issues](https://github.com/adityachandelgit/BookLore/issues) -- Include reproduction steps, expected vs. actual behavior, and logs if possible +Found a bug? Help us fix it by providing detailed information: -### ๐ŸŒŸ Feature Requests +1. **Search existing issues** to avoid duplicates +2. **Create a new issue** with the `bug` label +3. **Include the following:** + - Clear, descriptive title (e.g., "Book import fails with PDF files over 100MB") + - Steps to reproduce the issue + - Expected behavior vs. actual behavior + - Screenshots or error logs if applicable + - Your environment (OS, browser, Docker version, etc.) -- Clearly explain the use case and benefit -- Label the issue with `feature` +**Example Bug Report:** +```markdown +**Title:** Book metadata not updating after manual edit -### ๐Ÿ”ƒ Code Contributions +**Description:** +When I manually edit a book's metadata through the UI and click Save, +the changes appear to save but revert after page refresh. -- Create a feature branch: +**Steps to Reproduce:** +1. Navigate to any book detail page +2. Click "Edit Metadata" +3. Change the title from "Old Title" to "New Title" +4. Click "Save" +5. Refresh the page -```bash -git checkout -b feat/my-feature +**Expected:** Title should remain "New Title" +**Actual:** Title reverts to "Old Title" + +**Environment:** +- Browser: Chrome 120 +- OS: macOS 14.2 +- Booklore Version: 1.2.0 ``` -- For bug fixes: +--- + +### ๐Ÿ”ƒ Submitting Code Changes + +#### Branch Naming Convention + +Create descriptive branches that clearly indicate the purpose of your changes: ```bash -git checkout -b fix/my-fix +# For new features +git checkout -b feat/add-dark-mode-theme +git checkout -b feat/epub-reader-support + +# For bug fixes +git checkout -b fix/book-import-validation +git checkout -b fix/memory-leak-in-scanner + +# For documentation +git checkout -b docs/update-installation-guide + +# For refactoring +git checkout -b refactor/improve-authentication-flow ``` -- Follow code conventions, keep PRs focused and scoped -- Link the relevant issue in your PR -- Test your changes -- Target the `develop` branch when opening PRs +#### Development Workflow + +1. **Create a branch** from `develop` (not `main`) +2. **Make your changes** in small, logical commits +3. **Test thoroughly** - run both frontend and backend tests +4. **Update documentation** if your changes affect usage +5. **Run the linter** and fix any issues +6. **Commit with clear messages** following Conventional Commits +7. **Push to your fork** +8. **Open a pull request** targeting the `develop` branch + +#### Pull Request Checklist + +Before submitting, ensure: +- [ ] Code follows project conventions +- [ ] All tests pass (`./gradlew test` for backend) +- [ ] IntelliJ linter shows no errors +- [ ] Changes are documented (README, inline comments) +- [ ] PR description clearly explains what and why +- [ ] PR is linked to a related issue (if applicable) +- [ ] Branch is up-to-date with `develop` +- [ ] **For big features:** Create a documentation PR at [booklore-docs](https://github.com/booklore-app/booklore-docs) with styling similar to other documentation pages --- ## ๐Ÿงผ Code Style & Conventions - **Angular**: Follow the [official style guide](https://angular.io/guide/styleguide) -- **Java**: Use modern features (Java 17+), clean structure -- **Format**: Use linters and Prettier where applicable -- **UI**: Use Tailwind CSS and PrimeNG components consistently +- **Java**: Use modern features (Java 21), clean structure +- **Linter**: Use IntelliJ IDEA's built-in linter for code formatting and style checks +- **UI**: Use SCSS and PrimeNG components consistently --- ## ๐Ÿ“ Commit Message Format -Follow [Conventional Commits](https://www.conventionalcommits.org/): +We follow [Conventional Commits](https://www.conventionalcommits.org/) for clear, standardized commit messages. -Examples: +### Format -- `feat: add column visibility setting to book table` -- `fix: correct metadata locking behavior` -- `docs: improve contributing instructions` +``` +(): + +[optional body] + +[optional footer] +``` + +### Types + +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, no logic change) +- `refactor`: Code refactoring +- `test`: Adding or updating tests +- `chore`: Maintenance tasks +- `perf`: Performance improvements + +### Examples + +```bash +# Feature addition +feat(reader): add keyboard navigation for page turning + +# Bug fix +fix(api): resolve memory leak in book scanning service + +# Documentation +docs(readme): add troubleshooting section for Docker setup + +# Multiple scopes +feat(api,ui): implement book collection management + +# Breaking change +feat(auth)!: migrate to OAuth 2.1 + +BREAKING CHANGE: OAuth 2.0 is no longer supported +``` --- ## ๐Ÿ™ Code of Conduct -Please be respectful, inclusive, and collaborative. Harassment, abuse, or discrimination of any kind will not be tolerated. +We're committed to providing a welcoming and inclusive environment for everyone. + +**Our Standards:** +- โœ… Be respectful and considerate +- โœ… Welcome newcomers and help them learn +- โœ… Accept constructive criticism gracefully +- โœ… Focus on what's best for the community + +**Unacceptable Behavior:** +- โŒ Harassment, trolling, or discrimination +- โŒ Personal attacks or insults +- โŒ Publishing others' private information +- โŒ Any conduct that would be inappropriate in a professional setting + +**Enforcement:** +Instances of unacceptable behavior may result in temporary or permanent ban from the project. --- ## ๐Ÿ’ฌ Community & Support -- Discord server: https://discord.gg/Ee5hd458Uz +**Need help or want to discuss ideas?** + +- ๐Ÿ’ฌ **Discord**: [Join our server](https://discord.gg/Ee5hd458Uz) +- ๐Ÿ› **Issues**: [GitHub Issues](https://github.com/adityachandelgit/BookLore/issues) --- ## ๐Ÿ“„ License -Booklore is open-source and licensed under the GPL-3.0 License. See [`LICENSE`](./LICENSE) for details. +Booklore is open-source software licensed under the **GPL-3.0 License**. + +By contributing, you agree that your contributions will be licensed under the same license. See the [`LICENSE`](./LICENSE) file for full details. --- -Happy contributing! +## ๐ŸŽฏ What to Work On? + +Not sure where to start? Check out: + +- Issues labeled [`good first issue`](https://github.com/adityachandelgit/BookLore/labels/good%20first%20issue) +- Issues labeled [`help wanted`](https://github.com/adityachandelgit/BookLore/labels/help%20wanted) +- Our [project roadmap](https://github.com/adityachandelgit/BookLore/projects) + +--- + +## ๐ŸŽ‰ Thank You! + +Every contribution, no matter how small, makes Booklore better. Thank you for being part of our community! + +**Happy Contributing! ๐Ÿ“šโœจ** diff --git a/Dockerfile b/Dockerfile index dfa920da..79c8c7bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,10 @@ FROM node:22-alpine AS angular-build WORKDIR /angular-app COPY ./booklore-ui/package.json ./booklore-ui/package-lock.json ./ -RUN npm config set registry http://registry.npmjs.org/ \ - && npm config set fetch-retries 5 \ - && npm config set fetch-retry-mintimeout 20000 \ - && npm config set fetch-retry-maxtimeout 120000 \ - && npm install --force +RUN --mount=type=cache,target=/root/.npm \ + npm config set registry http://registry.npmjs.org/ \ + && npm ci --force + COPY ./booklore-ui /angular-app/ RUN npm run build --configuration=production @@ -18,7 +17,13 @@ FROM gradle:8.14.3-jdk21-alpine AS springboot-build WORKDIR /springboot-app +# Copy only build files first to cache dependencies COPY ./booklore-api/build.gradle ./booklore-api/settings.gradle /springboot-app/ + +# Download dependencies (cached layer) +RUN --mount=type=cache,target=/home/gradle/.gradle \ + gradle dependencies --no-daemon + COPY ./booklore-api/src /springboot-app/src # Inject version into application.yaml using yq @@ -26,7 +31,8 @@ ARG APP_VERSION RUN apk add --no-cache yq && \ yq eval '.app.version = strenv(APP_VERSION)' -i /springboot-app/src/main/resources/application.yaml -RUN gradle clean build -x test +RUN --mount=type=cache,target=/home/gradle/.gradle \ + gradle clean build -x test --no-daemon --parallel # Stage 3: Final image FROM eclipse-temurin:21.0.9_10-jre-alpine diff --git a/booklore-api/build.gradle b/booklore-api/build.gradle index 49400fee..d45283a8 100644 --- a/booklore-api/build.gradle +++ b/booklore-api/build.gradle @@ -39,7 +39,7 @@ dependencies { // --- Database & Migration --- implementation 'org.mariadb.jdbc:mariadb-java-client:3.5.6' - implementation 'org.flywaydb:flyway-mysql:11.18.0' + implementation 'org.flywaydb:flyway-mysql:11.19.0' // --- Security & Authentication --- implementation 'io.jsonwebtoken:jjwt-api:0.13.0' @@ -73,14 +73,14 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14' implementation 'org.apache.commons:commons-compress:1.28.0' implementation 'org.tukaani:xz:1.11' // Required by commons-compress for 7z support - implementation 'org.apache.commons:commons-text:1.14.0' + implementation 'org.apache.commons:commons-text:1.15.0' // --- Template Engine --- - implementation 'org.freemarker:freemarker:2.3.33' + implementation 'org.freemarker:freemarker:2.3.34' // --- Test Dependencies --- testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.assertj:assertj-core:3.27.3' + testImplementation 'org.assertj:assertj-core:3.27.6' testImplementation "org.mockito:mockito-inline:5.2.0" } diff --git a/booklore-api/gradle/wrapper/gradle-wrapper.jar b/booklore-api/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c..1b33c55b 100644 Binary files a/booklore-api/gradle/wrapper/gradle-wrapper.jar and b/booklore-api/gradle/wrapper/gradle-wrapper.jar differ diff --git a/booklore-api/gradle/wrapper/gradle-wrapper.properties b/booklore-api/gradle/wrapper/gradle-wrapper.properties index 002b867c..d4081da4 100644 --- a/booklore-api/gradle/wrapper/gradle-wrapper.properties +++ b/booklore-api/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/booklore-api/gradlew b/booklore-api/gradlew index adff685a..23d15a93 100755 --- a/booklore-api/gradlew +++ b/booklore-api/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright ยฉ 2015 the original authors. +# Copyright ยฉ 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,6 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -171,6 +172,7 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -210,6 +212,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/booklore-api/gradlew.bat b/booklore-api/gradlew.bat index c4bdd3ab..db3a6ac2 100644 --- a/booklore-api/gradlew.bat +++ b/booklore-api/gradlew.bat @@ -70,10 +70,11 @@ goto fail :execute @rem Setup the command line +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookdropFileController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookdropFileController.java index 804fb9ae..c9395942 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookdropFileController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/BookdropFileController.java @@ -2,16 +2,23 @@ package com.adityachandel.booklore.controller; import com.adityachandel.booklore.model.dto.BookdropFile; import com.adityachandel.booklore.model.dto.BookdropFileNotification; +import com.adityachandel.booklore.model.dto.request.BookdropBulkEditRequest; import com.adityachandel.booklore.model.dto.request.BookdropFinalizeRequest; +import com.adityachandel.booklore.model.dto.request.BookdropPatternExtractRequest; import com.adityachandel.booklore.model.dto.request.BookdropSelectionRequest; +import com.adityachandel.booklore.model.dto.response.BookdropBulkEditResult; import com.adityachandel.booklore.model.dto.response.BookdropFinalizeResult; +import com.adityachandel.booklore.model.dto.response.BookdropPatternExtractResult; import com.adityachandel.booklore.service.bookdrop.BookDropService; +import com.adityachandel.booklore.service.bookdrop.BookdropBulkEditService; import com.adityachandel.booklore.service.bookdrop.BookdropMonitoringService; import com.adityachandel.booklore.service.monitoring.MonitoringService; +import com.adityachandel.booklore.service.bookdrop.FilenamePatternExtractor; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -26,6 +33,8 @@ public class BookdropFileController { private final BookDropService bookDropService; private final BookdropMonitoringService monitoringService; + private final FilenamePatternExtractor filenamePatternExtractor; + private final BookdropBulkEditService bookdropBulkEditService; @Operation(summary = "Get bookdrop notification summary", description = "Retrieve a summary of bookdrop file notifications.") @ApiResponse(responseCode = "200", description = "Notification summary returned successfully") @@ -68,4 +77,22 @@ public class BookdropFileController { monitoringService.rescanBookdropFolder(); return ResponseEntity.ok().build(); } + + @Operation(summary = "Extract metadata from filenames using pattern", description = "Parse filenames of selected files using a pattern to extract metadata fields.") + @ApiResponse(responseCode = "200", description = "Pattern extraction completed") + @PostMapping("/files/extract-pattern") + public ResponseEntity extractFromPattern( + @Parameter(description = "Pattern extraction request") @Valid @RequestBody BookdropPatternExtractRequest request) { + BookdropPatternExtractResult result = filenamePatternExtractor.bulkExtract(request); + return ResponseEntity.ok(result); + } + + @Operation(summary = "Bulk edit metadata for selected files", description = "Apply metadata changes to multiple selected files at once.") + @ApiResponse(responseCode = "200", description = "Bulk edit completed") + @PostMapping("/files/bulk-edit") + public ResponseEntity bulkEditMetadata( + @Parameter(description = "Bulk edit request") @Valid @RequestBody BookdropBulkEditRequest request) { + BookdropBulkEditResult result = bookdropBulkEditService.bulkEdit(request); + return ResponseEntity.ok(result); + } } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java index c484e368..ad0c35d1 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/MetadataController.java @@ -156,6 +156,27 @@ public class MetadataController { bookMetadataService.regenerateCover(bookId); } + @Operation(summary = "Regenerate covers for selected books", description = "Regenerate covers for a list of books. Requires metadata edit permission or admin.") + @ApiResponse(responseCode = "204", description = "Cover regeneration started successfully") + @PostMapping("/bulk-regenerate-covers") + @PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()") + public ResponseEntity regenerateCoversForBooks( + @Parameter(description = "List of book IDs") @Validated @RequestBody BulkBookIdsRequest request) { + bookMetadataService.regenerateCoversForBooks(request.getBookIds()); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "Upload cover image for multiple books", description = "Upload a cover image to apply to multiple books. Requires metadata edit permission or admin.") + @ApiResponse(responseCode = "204", description = "Cover upload started successfully") + @PostMapping("/bulk-upload-cover") + @PreAuthorize("@securityUtil.canEditMetadata() or @securityUtil.isAdmin()") + public ResponseEntity bulkUploadCover( + @Parameter(description = "Cover image file") @RequestParam("file") MultipartFile file, + @Parameter(description = "Comma-separated book IDs") @RequestParam("bookIds") @jakarta.validation.constraints.NotEmpty java.util.Set bookIds) { + bookMetadataService.updateCoverImageFromFileForBooks(bookIds, file); + return ResponseEntity.noContent().build(); + } + @Operation(summary = "Recalculate metadata match scores", description = "Recalculate match scores for all metadata. Requires admin.") @ApiResponse(responseCode = "204", description = "Match scores recalculated successfully") @PostMapping("/metadata/recalculate-match-scores") diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsController.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsController.java index 107bc59a..bd1a11f5 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsController.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsController.java @@ -107,6 +107,16 @@ public class OpdsController { .body(feed); } + @Operation(summary = "Get OPDS series navigation", description = "Retrieve the OPDS series navigation feed.") + @ApiResponse(responseCode = "200", description = "Series navigation feed returned successfully") + @GetMapping(value = "/series", produces = OPDS_CATALOG_MEDIA_TYPE) + public ResponseEntity getSeriesNavigation(@Parameter(hidden = true) HttpServletRequest request) { + String feed = opdsFeedService.generateSeriesNavigation(request); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(OPDS_CATALOG_MEDIA_TYPE)) + .body(feed); + } + @Operation(summary = "Get OPDS catalog feed", description = "Retrieve the OPDS acquisition catalog feed.") @ApiResponse(responseCode = "200", description = "Catalog feed returned successfully") @GetMapping(value = "/catalog", produces = OPDS_ACQUISITION_MEDIA_TYPE) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsUserV2Controller.java b/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsUserV2Controller.java index 3224db80..763ce9ce 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsUserV2Controller.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/controller/OpdsUserV2Controller.java @@ -2,6 +2,7 @@ package com.adityachandel.booklore.controller; import com.adityachandel.booklore.model.dto.OpdsUserV2; import com.adityachandel.booklore.model.dto.request.OpdsUserV2CreateRequest; +import com.adityachandel.booklore.model.dto.request.OpdsUserV2UpdateRequest; import com.adityachandel.booklore.service.opds.OpdsUserV2Service; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -46,4 +47,13 @@ public class OpdsUserV2Controller { @Parameter(description = "ID of the OPDS user to delete") @PathVariable Long id) { service.deleteOpdsUser(id); } -} + + @Operation(summary = "Update OPDS user", description = "Update an OPDS user's settings by ID.") + @ApiResponse(responseCode = "200", description = "OPDS user updated successfully") + @PatchMapping("/{id}") + @PreAuthorize("@securityUtil.isAdmin() or @securityUtil.canAccessOpds()") + public OpdsUserV2 updateUser( + @Parameter(description = "ID of the OPDS user to update") @PathVariable Long id, + @Parameter(description = "OPDS user update request") @RequestBody OpdsUserV2UpdateRequest updateRequest) { + return service.updateOpdsUser(id, updateRequest); + }} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java b/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java index 1ecca03e..f4ed7862 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/exception/ApiError.java @@ -55,7 +55,7 @@ public enum ApiError { SHELF_CANNOT_BE_DELETED(HttpStatus.FORBIDDEN, "'%s' shelf can't be deleted" ), TASK_NOT_FOUND(HttpStatus.NOT_FOUND, "Scheduled task not found: %s"), TASK_ALREADY_RUNNING(HttpStatus.CONFLICT, "Task is already running: %s"), - ICON_ALREADY_EXISTS(HttpStatus.CONFLICT, "SVG icon with name '%s' already exists"),; + ICON_ALREADY_EXISTS(HttpStatus.CONFLICT, "SVG icon with name '%s' already exists"); private final HttpStatus status; private final String message; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java index d15ce196..4a1548fe 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/MetadataClearFlags.java @@ -19,6 +19,7 @@ public class MetadataClearFlags { private boolean goodreadsId; private boolean comicvineId; private boolean hardcoverId; + private boolean hardcoverBookId; private boolean googleId; private boolean pageCount; private boolean language; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java index 2cb7d65d..312648eb 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookMetadata.java @@ -37,6 +37,7 @@ public class BookMetadata { private Double goodreadsRating; private Integer goodreadsReviewCount; private String hardcoverId; + private Integer hardcoverBookId; private Double hardcoverRating; private Integer hardcoverReviewCount; private String doubanId; @@ -66,6 +67,7 @@ public class BookMetadata { private Boolean goodreadsIdLocked; private Boolean comicvineIdLocked; private Boolean hardcoverIdLocked; + private Boolean hardcoverBookIdLocked; private Boolean doubanIdLocked; private Boolean googleIdLocked; private Boolean pageCountLocked; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/OpdsUserV2.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/OpdsUserV2.java index 11e78ae5..3f39b710 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/OpdsUserV2.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/OpdsUserV2.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.dto; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; @@ -14,4 +15,5 @@ public class OpdsUserV2 { private String username; @JsonIgnore private String passwordHash; + private OpdsSortOrder sortOrder; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropBulkEditRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropBulkEditRequest.java new file mode 100644 index 00000000..ca93756f --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropBulkEditRequest.java @@ -0,0 +1,20 @@ +package com.adityachandel.booklore.model.dto.request; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import lombok.Data; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Set; + +@Data +public class BookdropBulkEditRequest { + @NotNull + private BookMetadata fields; + @NotNull + private Set enabledFields; + private boolean mergeArrays; + private boolean selectAll; + private List excludedIds; + private List selectedIds; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropPatternExtractRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropPatternExtractRequest.java new file mode 100644 index 00000000..58f81b7a --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BookdropPatternExtractRequest.java @@ -0,0 +1,16 @@ +package com.adityachandel.booklore.model.dto.request; + +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; + +@Data +public class BookdropPatternExtractRequest { + @NotBlank + private String pattern; + private Boolean selectAll; + private List excludedIds; + private List selectedIds; + private Boolean preview; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BulkBookIdsRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BulkBookIdsRequest.java new file mode 100644 index 00000000..eb8757d4 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/BulkBookIdsRequest.java @@ -0,0 +1,12 @@ +package com.adityachandel.booklore.model.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Set; + +@Data +public class BulkBookIdsRequest { + @NotEmpty(message = "At least one book ID is required") + private Set bookIds; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2CreateRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2CreateRequest.java index 88bb8664..eb087871 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2CreateRequest.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2CreateRequest.java @@ -1,9 +1,11 @@ package com.adityachandel.booklore.model.dto.request; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import lombok.Data; @Data public class OpdsUserV2CreateRequest { private String username; private String password; + private OpdsSortOrder sortOrder; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2UpdateRequest.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2UpdateRequest.java new file mode 100644 index 00000000..14b3b8fa --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/OpdsUserV2UpdateRequest.java @@ -0,0 +1,10 @@ +package com.adityachandel.booklore.model.dto.request; + +import com.adityachandel.booklore.model.enums.OpdsSortOrder; +import jakarta.validation.constraints.NotNull; + +public record OpdsUserV2UpdateRequest( + @NotNull(message = "Sort order is required") + OpdsSortOrder sortOrder +) { +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropBulkEditResult.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropBulkEditResult.java new file mode 100644 index 00000000..40c9089b --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropBulkEditResult.java @@ -0,0 +1,12 @@ +package com.adityachandel.booklore.model.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class BookdropBulkEditResult { + private int totalFiles; + private int successfullyUpdated; + private int failed; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropPatternExtractResult.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropPatternExtractResult.java new file mode 100644 index 00000000..c4ba5582 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/response/BookdropPatternExtractResult.java @@ -0,0 +1,26 @@ +package com.adityachandel.booklore.model.dto.response; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class BookdropPatternExtractResult { + private int totalFiles; + private int successfullyExtracted; + private int failed; + private List results; + + @Data + @Builder + public static class FileExtractionResult { + private Long fileId; + private String fileName; + private boolean success; + private BookMetadata extractedMetadata; + private String errorMessage; + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettingKey.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettingKey.java index 35cee670..7add6c81 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettingKey.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettingKey.java @@ -16,6 +16,7 @@ public enum AppSettingKey { METADATA_PERSISTENCE_SETTINGS("metadata_persistence_settings", true, false), METADATA_PUBLIC_REVIEWS_SETTINGS("metadata_public_reviews_settings", true, false), KOBO_SETTINGS("kobo_settings", true, false), + COVER_CROPPING_SETTINGS("cover_cropping_settings", true, false), AUTO_BOOK_SEARCH("auto_book_search", false, false), COVER_IMAGE_RESOLUTION("cover_image_resolution", false, false), diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettings.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettings.java index 792b6b3d..1e2c650a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettings.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/AppSettings.java @@ -33,4 +33,5 @@ public class AppSettings { private MetadataPersistenceSettings metadataPersistenceSettings; private MetadataPublicReviewsSettings metadataPublicReviewsSettings; private KoboSettings koboSettings; + private CoverCroppingSettings coverCroppingSettings; } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/CoverCroppingSettings.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/CoverCroppingSettings.java new file mode 100644 index 00000000..d5e7fc20 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/CoverCroppingSettings.java @@ -0,0 +1,13 @@ +package com.adityachandel.booklore.model.dto.settings; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class CoverCroppingSettings { + private boolean verticalCroppingEnabled; + private boolean horizontalCroppingEnabled; + private double aspectRatioThreshold; + private boolean smartCroppingEnabled; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java index 1a6f1138..47c0e1f2 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java @@ -11,4 +11,5 @@ public class KoboSettings { private boolean convertCbxToEpub; private int conversionLimitInMbForCbx; private boolean forceEnableHyphenation; + private int conversionImageCompressionPercentage; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java index 5ad6dc6a..19b3ab99 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/BookMetadataEntity.java @@ -97,6 +97,9 @@ public class BookMetadataEntity { @Column(name = "hardcover_id", length = 100) private String hardcoverId; + @Column(name = "hardcover_book_id") + private Integer hardcoverBookId; + @Column(name = "google_id", length = 100) private String googleId; @@ -208,6 +211,10 @@ public class BookMetadataEntity { @Builder.Default private Boolean hardcoverIdLocked = Boolean.FALSE; + @Column(name = "hardcover_book_id_locked") + @Builder.Default + private Boolean hardcoverBookIdLocked = Boolean.FALSE; + @Column(name = "google_id_locked") @Builder.Default private Boolean googleIdLocked = Boolean.FALSE; @@ -309,6 +316,7 @@ public class BookMetadataEntity { this.comicvineIdLocked = lock; this.goodreadsIdLocked = lock; this.hardcoverIdLocked = lock; + this.hardcoverBookIdLocked = lock; this.googleIdLocked = lock; this.reviewsLocked = lock; } @@ -341,6 +349,7 @@ public class BookMetadataEntity { && Boolean.TRUE.equals(this.goodreadsIdLocked) && Boolean.TRUE.equals(this.comicvineIdLocked) && Boolean.TRUE.equals(this.hardcoverIdLocked) + && Boolean.TRUE.equals(this.hardcoverBookIdLocked) && Boolean.TRUE.equals(this.googleIdLocked) && Boolean.TRUE.equals(this.reviewsLocked) ; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/OpdsUserV2Entity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/OpdsUserV2Entity.java index 82895eb4..118364bb 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/OpdsUserV2Entity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/OpdsUserV2Entity.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.entity; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import jakarta.persistence.*; import lombok.*; @@ -28,6 +29,11 @@ public class OpdsUserV2Entity { @Column(name = "password_hash", nullable = false) private String passwordHash; + @Enumerated(EnumType.STRING) + @Column(name = "sort_order", length = 20) + @Builder.Default + private OpdsSortOrder sortOrder = OpdsSortOrder.RECENT; + @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileExtension.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileExtension.java index 67d5f9a5..5186d61d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileExtension.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileExtension.java @@ -13,7 +13,8 @@ public enum BookFileExtension { EPUB("epub", BookFileType.EPUB), CBZ("cbz", BookFileType.CBX), CBR("cbr", BookFileType.CBX), - CB7("cb7", BookFileType.CBX); + CB7("cb7", BookFileType.CBX), + FB2("fb2", BookFileType.FB2); private final String extension; private final BookFileType type; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileType.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileType.java index b16361b3..21a9c9a2 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileType.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/BookFileType.java @@ -1,5 +1,5 @@ package com.adityachandel.booklore.model.enums; public enum BookFileType { - PDF, EPUB, CBX + PDF, EPUB, CBX, FB2 } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java new file mode 100644 index 00000000..6f5aa231 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java @@ -0,0 +1,13 @@ +package com.adityachandel.booklore.model.enums; + +public enum OpdsSortOrder { + RECENT, + TITLE_ASC, + TITLE_DESC, + AUTHOR_ASC, + AUTHOR_DESC, + SERIES_ASC, + SERIES_DESC, + RATING_ASC, + RATING_DESC +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookMetadataRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookMetadataRepository.java index 3ef971e8..2624c656 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookMetadataRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookMetadataRepository.java @@ -2,9 +2,12 @@ package com.adityachandel.booklore.repository; import com.adityachandel.booklore.model.entity.*; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import java.time.Instant; import java.util.List; public interface BookMetadataRepository extends JpaRepository { @@ -12,6 +15,11 @@ public interface BookMetadataRepository extends JpaRepository getMetadataForBookIds(@Param("bookIds") List bookIds); + @Modifying + @Transactional + @Query("UPDATE BookMetadataEntity m SET m.coverUpdatedOn = :timestamp WHERE m.bookId = :bookId") + void updateCoverTimestamp(@Param("bookId") Long bookId, @Param("timestamp") Instant timestamp); + List findAllByAuthorsContaining(AuthorEntity author); List findAllByCategoriesContaining(CategoryEntity category); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java index 8dd9fcb9..4b93afa3 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java @@ -162,4 +162,52 @@ public interface BookOpdsRepository extends JpaRepository, Jpa ORDER BY b.addedOn DESC """) Page findBookIdsByAuthorNameAndLibraryIds(@Param("authorName") String authorName, @Param("libraryIds") Collection libraryIds, Pageable pageable); + + // ============================================ + // SERIES - Distinct Series List + // ============================================ + + @Query(""" + SELECT DISTINCT m.seriesName FROM BookMetadataEntity m + JOIN m.book b + WHERE (b.deleted IS NULL OR b.deleted = false) + AND m.seriesName IS NOT NULL + AND m.seriesName != '' + ORDER BY m.seriesName + """) + List findDistinctSeries(); + + @Query(""" + SELECT DISTINCT m.seriesName FROM BookMetadataEntity m + JOIN m.book b + WHERE (b.deleted IS NULL OR b.deleted = false) + AND b.library.id IN :libraryIds + AND m.seriesName IS NOT NULL + AND m.seriesName != '' + ORDER BY m.seriesName + """) + List findDistinctSeriesByLibraryIds(@Param("libraryIds") Collection libraryIds); + + // ============================================ + // BOOKS BY SERIES - Two Query Pattern (sorted by series number) + // ============================================ + + @Query(""" + SELECT DISTINCT b.id FROM BookEntity b + JOIN b.metadata m + WHERE m.seriesName = :seriesName + AND (b.deleted IS NULL OR b.deleted = false) + ORDER BY COALESCE(m.seriesNumber, 999999), b.addedOn DESC + """) + Page findBookIdsBySeriesName(@Param("seriesName") String seriesName, Pageable pageable); + + @Query(""" + SELECT DISTINCT b.id FROM BookEntity b + JOIN b.metadata m + WHERE m.seriesName = :seriesName + AND b.library.id IN :libraryIds + AND (b.deleted IS NULL OR b.deleted = false) + ORDER BY COALESCE(m.seriesNumber, 999999), b.addedOn DESC + """) + Page findBookIdsBySeriesNameAndLibraryIds(@Param("seriesName") String seriesName, @Param("libraryIds") Collection libraryIds, Pageable pageable); } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/AppSettingService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/AppSettingService.java index 7ee03667..67415c22 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/AppSettingService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/AppSettingService.java @@ -95,6 +95,7 @@ public class AppSettingService { builder.metadataPersistenceSettings(settingPersistenceHelper.getJsonSetting(settingsMap, AppSettingKey.METADATA_PERSISTENCE_SETTINGS, MetadataPersistenceSettings.class, settingPersistenceHelper.getDefaultMetadataPersistenceSettings(), true)); builder.metadataPublicReviewsSettings(settingPersistenceHelper.getJsonSetting(settingsMap, AppSettingKey.METADATA_PUBLIC_REVIEWS_SETTINGS, MetadataPublicReviewsSettings.class, settingPersistenceHelper.getDefaultMetadataPublicReviewsSettings(), true)); builder.koboSettings(settingPersistenceHelper.getJsonSetting(settingsMap, AppSettingKey.KOBO_SETTINGS, KoboSettings.class, settingPersistenceHelper.getDefaultKoboSettings(), true)); + builder.coverCroppingSettings(settingPersistenceHelper.getJsonSetting(settingsMap, AppSettingKey.COVER_CROPPING_SETTINGS, CoverCroppingSettings.class, settingPersistenceHelper.getDefaultCoverCroppingSettings(), true)); builder.autoBookSearch(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.AUTO_BOOK_SEARCH, "true"))); builder.uploadPattern(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.UPLOAD_FILE_PATTERN, "{authors}/<{series}/><{seriesIndex}. >{title}< - {authors}>< ({year})>")); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java index 9548deee..cdbfbf0c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java @@ -255,7 +255,17 @@ public class SettingPersistenceHelper { .conversionLimitInMb(100) .convertCbxToEpub(false) .conversionLimitInMbForCbx(100) + .conversionImageCompressionPercentage(85) .forceEnableHyphenation(false) .build(); } + + public CoverCroppingSettings getDefaultCoverCroppingSettings() { + return CoverCroppingSettings.builder() + .verticalCroppingEnabled(false) + .horizontalCroppingEnabled(false) + .aspectRatioThreshold(2.5) + .smartCroppingEnabled(false) + .build(); + } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java index a5b93542..3ba1ae69 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java @@ -96,6 +96,7 @@ public class BookDownloadService { boolean convertEpubToKepub = isEpub && koboSettings.isConvertToKepub() && bookEntity.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMb() * 1024; boolean convertCbxToEpub = isCbx && koboSettings.isConvertCbxToEpub() && bookEntity.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMbForCbx() * 1024; + int compressionPercentage = koboSettings.getConversionImageCompressionPercentage(); Path tempDir = null; try { File inputFile = new File(FileUtils.getBookFullPath(bookEntity)); @@ -106,7 +107,7 @@ public class BookDownloadService { } if (convertCbxToEpub) { - fileToSend = cbxConversionService.convertCbxToEpub(inputFile, tempDir.toFile(), bookEntity); + fileToSend = cbxConversionService.convertCbxToEpub(inputFile, tempDir.toFile(), bookEntity,compressionPercentage); } if (convertEpubToKepub) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditService.java new file mode 100644 index 00000000..5b19bcee --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditService.java @@ -0,0 +1,138 @@ +package com.adityachandel.booklore.service.bookdrop; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.BookdropBulkEditRequest; +import com.adityachandel.booklore.model.dto.response.BookdropBulkEditResult; +import com.adityachandel.booklore.model.entity.BookdropFileEntity; +import com.adityachandel.booklore.repository.BookdropFileRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BookdropBulkEditService { + + private static final int BATCH_SIZE = 500; + + private final BookdropFileRepository bookdropFileRepository; + private final BookdropMetadataHelper metadataHelper; + + @Transactional + public BookdropBulkEditResult bulkEdit(BookdropBulkEditRequest request) { + List fileIds = metadataHelper.resolveFileIds( + request.isSelectAll(), + request.getExcludedIds(), + request.getSelectedIds() + ); + + return processBulkEditInBatches(fileIds, request); + } + + private BookdropBulkEditResult processBulkEditInBatches(List fileIds, BookdropBulkEditRequest request) { + int totalSuccessCount = 0; + int totalFailedCount = 0; + int totalFiles = fileIds.size(); + + for (int batchStart = 0; batchStart < fileIds.size(); batchStart += BATCH_SIZE) { + int batchEnd = Math.min(batchStart + BATCH_SIZE, fileIds.size()); + + BatchEditResult batchResult = processSingleBatch(fileIds, batchStart, batchEnd, request); + + totalSuccessCount += batchResult.successCount(); + totalFailedCount += batchResult.failureCount(); + + log.debug("Processed batch {}-{} of {}: {} successful, {} failed", + batchStart, batchEnd, totalFiles, batchResult.successCount(), batchResult.failureCount()); + } + + return BookdropBulkEditResult.builder() + .totalFiles(totalFiles) + .successfullyUpdated(totalSuccessCount) + .failed(totalFailedCount) + .build(); + } + + private BatchEditResult processSingleBatch(List allFileIds, int batchStart, int batchEnd, + BookdropBulkEditRequest request) { + List batchIds = allFileIds.subList(batchStart, batchEnd); + List batchFiles = bookdropFileRepository.findAllById(batchIds); + + int successCount = 0; + int failureCount = 0; + Set failedFileIds = new HashSet<>(); + + for (BookdropFileEntity file : batchFiles) { + try { + updateFileMetadata(file, request); + successCount++; + } catch (RuntimeException e) { + log.error("Failed to update metadata for file {} ({}): {}", + file.getId(), file.getFileName(), e.getMessage(), e); + failureCount++; + failedFileIds.add(file.getId()); + } + } + + List filesToSave = batchFiles.stream() + .filter(file -> !failedFileIds.contains(file.getId())) + .toList(); + + if (!filesToSave.isEmpty()) { + bookdropFileRepository.saveAll(filesToSave); + } + + return new BatchEditResult(successCount, failureCount); + } + + private void updateFileMetadata(BookdropFileEntity file, BookdropBulkEditRequest request) { + BookMetadata currentMetadata = metadataHelper.getCurrentMetadata(file); + BookMetadata updates = request.getFields(); + Set enabledFields = request.getEnabledFields(); + boolean mergeArrays = request.isMergeArrays(); + + if (enabledFields.contains("seriesName") && updates.getSeriesName() != null) { + currentMetadata.setSeriesName(updates.getSeriesName()); + } + if (enabledFields.contains("seriesTotal") && updates.getSeriesTotal() != null) { + currentMetadata.setSeriesTotal(updates.getSeriesTotal()); + } + if (enabledFields.contains("publisher") && updates.getPublisher() != null) { + currentMetadata.setPublisher(updates.getPublisher()); + } + if (enabledFields.contains("language") && updates.getLanguage() != null) { + currentMetadata.setLanguage(updates.getLanguage()); + } + + updateArrayField("authors", enabledFields, currentMetadata.getAuthors(), updates.getAuthors(), + currentMetadata::setAuthors, mergeArrays); + updateArrayField("categories", enabledFields, currentMetadata.getCategories(), updates.getCategories(), + currentMetadata::setCategories, mergeArrays); + updateArrayField("moods", enabledFields, currentMetadata.getMoods(), updates.getMoods(), + currentMetadata::setMoods, mergeArrays); + updateArrayField("tags", enabledFields, currentMetadata.getTags(), updates.getTags(), + currentMetadata::setTags, mergeArrays); + + metadataHelper.updateFetchedMetadata(file, currentMetadata); + } + + private void updateArrayField(String fieldName, Set enabledFields, + Set currentValue, Set newValue, + java.util.function.Consumer> setter, boolean mergeArrays) { + if (enabledFields.contains(fieldName) && newValue != null) { + if (mergeArrays && currentValue != null) { + Set merged = new LinkedHashSet<>(currentValue); + merged.addAll(newValue); + setter.accept(merged); + } else { + setter.accept(newValue); + } + } + } + + private record BatchEditResult(int successCount, int failureCount) {} +} \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropMetadataHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropMetadataHelper.java new file mode 100644 index 00000000..b6df3755 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/BookdropMetadataHelper.java @@ -0,0 +1,70 @@ +package com.adityachandel.booklore.service.bookdrop; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.entity.BookdropFileEntity; +import com.adityachandel.booklore.repository.BookdropFileRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class BookdropMetadataHelper { + + private final BookdropFileRepository bookdropFileRepository; + private final ObjectMapper objectMapper; + + public List resolveFileIds(boolean selectAll, List excludedIds, List selectedIds) { + if (selectAll) { + List excluded = excludedIds != null ? excludedIds : Collections.emptyList(); + if (excluded.isEmpty()) { + return bookdropFileRepository.findAllIds(); + } else { + return bookdropFileRepository.findAllExcludingIdsFlat(excluded); + } + } + return selectedIds != null ? selectedIds : Collections.emptyList(); + } + + public BookMetadata getCurrentMetadata(BookdropFileEntity file) { + try { + String fetchedMetadataJson = file.getFetchedMetadata(); + if (fetchedMetadataJson != null && !fetchedMetadataJson.isBlank()) { + return objectMapper.readValue(fetchedMetadataJson, BookMetadata.class); + } + } catch (Exception e) { + log.error("Error parsing existing metadata for file {}: {}", file.getId(), e.getMessage()); + } + return new BookMetadata(); + } + + public void updateFetchedMetadata(BookdropFileEntity file, BookMetadata metadata) { + try { + String updatedMetadataJson = objectMapper.writeValueAsString(metadata); + file.setFetchedMetadata(updatedMetadataJson); + } catch (Exception e) { + log.error("Error serializing metadata for file {}: {}", file.getId(), e.getMessage()); + throw new RuntimeException("Failed to update metadata", e); + } + } + + public void mergeMetadata(BookMetadata target, BookMetadata source) { + if (source.getSeriesName() != null) target.setSeriesName(source.getSeriesName()); + if (source.getTitle() != null) target.setTitle(source.getTitle()); + if (source.getSubtitle() != null) target.setSubtitle(source.getSubtitle()); + if (source.getAuthors() != null && !source.getAuthors().isEmpty()) target.setAuthors(source.getAuthors()); + if (source.getSeriesNumber() != null) target.setSeriesNumber(source.getSeriesNumber()); + if (source.getPublishedDate() != null) target.setPublishedDate(source.getPublishedDate()); + if (source.getPublisher() != null) target.setPublisher(source.getPublisher()); + if (source.getLanguage() != null) target.setLanguage(source.getLanguage()); + if (source.getSeriesTotal() != null) target.setSeriesTotal(source.getSeriesTotal()); + if (source.getIsbn10() != null) target.setIsbn10(source.getIsbn10()); + if (source.getIsbn13() != null) target.setIsbn13(source.getIsbn13()); + if (source.getAsin() != null) target.setAsin(source.getAsin()); + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractor.java new file mode 100644 index 00000000..1bb20c49 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractor.java @@ -0,0 +1,630 @@ +package com.adityachandel.booklore.service.bookdrop; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.BookdropPatternExtractRequest; +import com.adityachandel.booklore.model.dto.response.BookdropPatternExtractResult; +import com.adityachandel.booklore.model.entity.BookdropFileEntity; +import com.adityachandel.booklore.repository.BookdropFileRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.annotation.PreDestroy; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.*; +import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FilenamePatternExtractor { + + private final BookdropFileRepository bookdropFileRepository; + private final BookdropMetadataHelper metadataHelper; + private final ExecutorService regexExecutor = Executors.newCachedThreadPool(runnable -> { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + return thread; + }); + + private static final int PREVIEW_FILE_LIMIT = 5; + private static final long REGEX_TIMEOUT_SECONDS = 5; + private static final int TWO_DIGIT_YEAR_CUTOFF = 50; + private static final int TWO_DIGIT_YEAR_CENTURY_BASE = 1900; + private static final int FOUR_DIGIT_YEAR_LENGTH = 4; + private static final int TWO_DIGIT_YEAR_LENGTH = 2; + private static final int COMPACT_DATE_LENGTH = 8; + + private static final Map PLACEHOLDER_CONFIGS = Map.ofEntries( + Map.entry("SeriesName", new PlaceholderConfig("(.+?)", "seriesName")), + Map.entry("Title", new PlaceholderConfig("(.+?)", "title")), + Map.entry("Subtitle", new PlaceholderConfig("(.+?)", "subtitle")), + Map.entry("Authors", new PlaceholderConfig("(.+?)", "authors")), + Map.entry("SeriesNumber", new PlaceholderConfig("(\\d+(?:\\.\\d+)?)", "seriesNumber")), + Map.entry("Published", new PlaceholderConfig("(.+?)", "publishedDate")), + Map.entry("Publisher", new PlaceholderConfig("(.+?)", "publisher")), + Map.entry("Language", new PlaceholderConfig("([a-zA-Z]+)", "language")), + Map.entry("SeriesTotal", new PlaceholderConfig("(\\d+)", "seriesTotal")), + Map.entry("ISBN10", new PlaceholderConfig("(\\d{9}[0-9Xx])", "isbn10")), + Map.entry("ISBN13", new PlaceholderConfig("([0-9]{13})", "isbn13")), + Map.entry("ASIN", new PlaceholderConfig("(B[A-Za-z0-9]{9}|\\d{9}[0-9Xx])", "asin")) + ); + + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\w+)(?::(.*?))?}|\\*"); + + private static final Pattern FOUR_DIGIT_YEAR_PATTERN = Pattern.compile("\\d{4}"); + private static final Pattern TWO_DIGIT_YEAR_PATTERN = Pattern.compile("\\d{2}"); + private static final Pattern COMPACT_DATE_PATTERN = Pattern.compile("\\d{8}"); + private static final Pattern FLEXIBLE_DATE_PATTERN = Pattern.compile("(\\d{1,4})([^\\d])(\\d{1,2})\\2(\\d{1,4})"); + + @Transactional + public BookdropPatternExtractResult bulkExtract(BookdropPatternExtractRequest request) { + List fileIds = metadataHelper.resolveFileIds( + Boolean.TRUE.equals(request.getSelectAll()), + request.getExcludedIds(), + request.getSelectedIds() + ); + + boolean isPreview = Boolean.TRUE.equals(request.getPreview()); + ParsedPattern cachedPattern = parsePattern(request.getPattern()); + + if (cachedPattern == null) { + log.error("Failed to parse pattern: '{}'", request.getPattern()); + return buildEmptyResult(fileIds.size()); + } + + return isPreview + ? processPreviewExtraction(fileIds, cachedPattern) + : processFullExtractionInBatches(fileIds, cachedPattern); + } + + private BookdropPatternExtractResult processPreviewExtraction(List fileIds, ParsedPattern pattern) { + List limitedFileIds = fileIds.size() > PREVIEW_FILE_LIMIT + ? fileIds.subList(0, PREVIEW_FILE_LIMIT) + : fileIds; + + List previewFiles = bookdropFileRepository.findAllById(limitedFileIds); + List results = new ArrayList<>(); + int successCount = 0; + + for (BookdropFileEntity file : previewFiles) { + BookdropPatternExtractResult.FileExtractionResult result = extractFromFile(file, pattern); + results.add(result); + if (result.isSuccess()) { + successCount++; + } + } + + int failureCount = previewFiles.size() - successCount; + + return BookdropPatternExtractResult.builder() + .totalFiles(fileIds.size()) + .successfullyExtracted(successCount) + .failed(failureCount) + .results(results) + .build(); + } + + private BookdropPatternExtractResult processFullExtractionInBatches(List fileIds, ParsedPattern pattern) { + final int BATCH_SIZE = 500; + List allResults = new ArrayList<>(); + int totalSuccessCount = 0; + int totalFailureCount = 0; + int totalFiles = fileIds.size(); + + for (int batchStart = 0; batchStart < fileIds.size(); batchStart += BATCH_SIZE) { + int batchEnd = Math.min(batchStart + BATCH_SIZE, fileIds.size()); + + BatchExtractionResult batchResult = processSingleExtractionBatch(fileIds, batchStart, batchEnd, pattern); + + allResults.addAll(batchResult.results()); + totalSuccessCount += batchResult.successCount(); + totalFailureCount += batchResult.failureCount(); + + log.debug("Processed pattern extraction batch {}-{} of {}: {} successful, {} failed", + batchStart, batchEnd, totalFiles, batchResult.successCount(), batchResult.failureCount()); + } + + return BookdropPatternExtractResult.builder() + .totalFiles(totalFiles) + .successfullyExtracted(totalSuccessCount) + .failed(totalFailureCount) + .results(allResults) + .build(); + } + + private BatchExtractionResult processSingleExtractionBatch(List allFileIds, int batchStart, + int batchEnd, ParsedPattern pattern) { + List batchIds = allFileIds.subList(batchStart, batchEnd); + List batchFiles = bookdropFileRepository.findAllById(batchIds); + List batchResults = new ArrayList<>(); + + for (BookdropFileEntity file : batchFiles) { + BookdropPatternExtractResult.FileExtractionResult result = extractFromFile(file, pattern); + batchResults.add(result); + } + + persistExtractedMetadata(batchResults, batchFiles); + + int successCount = (int) batchResults.stream().filter(BookdropPatternExtractResult.FileExtractionResult::isSuccess).count(); + int failureCount = batchFiles.size() - successCount; + return new BatchExtractionResult(batchResults, successCount, failureCount); + } + + private BookdropPatternExtractResult buildEmptyResult(int totalFiles) { + return BookdropPatternExtractResult.builder() + .totalFiles(totalFiles) + .successfullyExtracted(0) + .failed(totalFiles) + .results(Collections.emptyList()) + .build(); + } + + public BookMetadata extractFromFilename(String filename, String pattern) { + ParsedPattern parsedPattern = parsePattern(pattern); + if (parsedPattern == null) { + return null; + } + + return extractFromFilenameWithParsedPattern(filename, parsedPattern); + } + + private BookMetadata extractFromFilenameWithParsedPattern(String filename, ParsedPattern parsedPattern) { + String nameOnly = FilenameUtils.getBaseName(filename); + + Optional matcherResult = executeRegexMatchingWithTimeout(parsedPattern.compiledPattern(), nameOnly); + + if (matcherResult.isEmpty()) { + return null; + } + + Matcher matcher = matcherResult.get(); + return buildMetadataFromMatch(matcher, parsedPattern.placeholderOrder()); + } + + private Optional executeRegexMatchingWithTimeout(Pattern pattern, String input) { + Future> future = regexExecutor.submit(() -> { + Matcher matcher = pattern.matcher(input); + return matcher.find() ? Optional.of(matcher) : Optional.empty(); + }); + + try { + return future.get(REGEX_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); + log.warn("Pattern matching exceeded {} second timeout for: {}", + REGEX_TIMEOUT_SECONDS, input.substring(0, Math.min(50, input.length()))); + return Optional.empty(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Optional.empty(); + } catch (ExecutionException e) { + log.error("Pattern matching failed: {}", e.getCause() != null ? e.getCause().getMessage() : "Unknown"); + return Optional.empty(); + } + } + + @PreDestroy + public void shutdownRegexExecutor() { + regexExecutor.shutdown(); + try { + if (!regexExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + regexExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + regexExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + private BookdropPatternExtractResult.FileExtractionResult extractFromFile( + BookdropFileEntity file, + ParsedPattern parsedPattern) { + try { + BookMetadata extracted = extractFromFilenameWithParsedPattern(file.getFileName(), parsedPattern); + + if (extracted == null) { + String errorMsg = "Pattern did not match filename structure. Check if the pattern aligns with the filename format."; + log.debug("Pattern mismatch for file '{}'", file.getFileName()); + return BookdropPatternExtractResult.FileExtractionResult.builder() + .fileId(file.getId()) + .fileName(file.getFileName()) + .success(false) + .errorMessage(errorMsg) + .build(); + } + + return BookdropPatternExtractResult.FileExtractionResult.builder() + .fileId(file.getId()) + .fileName(file.getFileName()) + .success(true) + .extractedMetadata(extracted) + .build(); + + } catch (RuntimeException e) { + String errorMsg = "Extraction failed: " + e.getMessage(); + log.debug("Pattern extraction failed for file '{}': {}", file.getFileName(), e.getMessage()); + return BookdropPatternExtractResult.FileExtractionResult.builder() + .fileId(file.getId()) + .fileName(file.getFileName()) + .success(false) + .errorMessage(errorMsg) + .build(); + } + } + + private ParsedPattern parsePattern(String pattern) { + if (pattern == null || pattern.isBlank()) { + return null; + } + + List placeholderMatches = findAllPlaceholders(pattern); + StringBuilder regexBuilder = new StringBuilder(); + List placeholderOrder = new ArrayList<>(); + int lastEnd = 0; + + for (int i = 0; i < placeholderMatches.size(); i++) { + PlaceholderMatch placeholderMatch = placeholderMatches.get(i); + + String literalTextBeforePlaceholder = pattern.substring(lastEnd, placeholderMatch.start); + regexBuilder.append(Pattern.quote(literalTextBeforePlaceholder)); + + String placeholderName = placeholderMatch.name; + String formatParameter = placeholderMatch.formatParameter; + + boolean isLastPlaceholder = (i == placeholderMatches.size() - 1); + boolean hasTextAfterPlaceholder = (placeholderMatch.end < pattern.length()); + boolean shouldUseGreedyMatching = isLastPlaceholder && !hasTextAfterPlaceholder; + + String regexForPlaceholder; + if ("*".equals(placeholderName)) { + regexForPlaceholder = shouldUseGreedyMatching ? "(.+)" : "(.+?)"; + } else if ("Published".equals(placeholderName) && formatParameter != null) { + regexForPlaceholder = buildRegexForDateFormat(formatParameter); + } else { + PlaceholderConfig config = PLACEHOLDER_CONFIGS.get(placeholderName); + regexForPlaceholder = determineRegexForPlaceholder(config, shouldUseGreedyMatching); + } + + regexBuilder.append(regexForPlaceholder); + + String placeholderWithFormat = formatParameter != null ? placeholderName + ":" + formatParameter : placeholderName; + placeholderOrder.add(placeholderWithFormat); + lastEnd = placeholderMatch.end; + } + + String literalTextAfterLastPlaceholder = pattern.substring(lastEnd); + regexBuilder.append(Pattern.quote(literalTextAfterLastPlaceholder)); + + try { + Pattern compiledPattern = Pattern.compile(regexBuilder.toString()); + return new ParsedPattern(compiledPattern, placeholderOrder); + } catch (PatternSyntaxException e) { + log.error("Invalid regex syntax from user input '{}': {}", pattern, e.getMessage()); + return null; + } + } + + private List findAllPlaceholders(String pattern) { + List placeholderMatches = new ArrayList<>(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(pattern); + + while (matcher.find()) { + String placeholderName; + String formatParameter = null; + + if (matcher.group(0).equals("*")) { + placeholderName = "*"; + } else { + placeholderName = matcher.group(1); + formatParameter = matcher.group(2); + } + + placeholderMatches.add(new PlaceholderMatch( + matcher.start(), + matcher.end(), + placeholderName, + formatParameter + )); + } + + return placeholderMatches; + } + + private String buildRegexForDateFormat(String dateFormat) { + StringBuilder result = new StringBuilder(); + int i = 0; + + while (i < dateFormat.length()) { + if (dateFormat.startsWith("yyyy", i)) { + result.append("\\d{4}"); + i += 4; + } else if (dateFormat.startsWith("yy", i)) { + result.append("\\d{2}"); + i += 2; + } else if (dateFormat.startsWith("MM", i)) { + result.append("\\d{2}"); + i += 2; + } else if (i < dateFormat.length() && dateFormat.charAt(i) == 'M') { + result.append("\\d{1,2}"); + i += 1; + } else if (dateFormat.startsWith("dd", i)) { + result.append("\\d{2}"); + i += 2; + } else if (i < dateFormat.length() && dateFormat.charAt(i) == 'd') { + result.append("\\d{1,2}"); + i += 1; + } else { + result.append(Pattern.quote(String.valueOf(dateFormat.charAt(i)))); + i++; + } + } + + return "(" + result.toString() + ")"; + } + + private String determineRegexForPlaceholder(PlaceholderConfig config, boolean shouldUseGreedyMatching) { + if (config != null) { + String configuredRegex = config.regex(); + boolean isNonGreedyTextPattern = configuredRegex.equals("(.+?)"); + + if (shouldUseGreedyMatching && isNonGreedyTextPattern) { + return "(.+)"; + } + return configuredRegex; + } + + return shouldUseGreedyMatching ? "(.+)" : "(.+?)"; + } + + private BookMetadata buildMetadataFromMatch(Matcher matcher, List placeholderOrder) { + BookMetadata metadata = new BookMetadata(); + + for (int i = 0; i < placeholderOrder.size(); i++) { + String placeholderWithFormat = placeholderOrder.get(i); + String[] parts = placeholderWithFormat.split(":", 2); + String placeholderName = parts[0]; + String formatParameter = parts.length > 1 ? parts[1] : null; + + if ("*".equals(placeholderName)) { + continue; + } + + String value = matcher.group(i + 1).trim(); + applyValueToMetadata(metadata, placeholderName, value, formatParameter); + } + + return metadata; + } + + private void applyValueToMetadata(BookMetadata metadata, String placeholderName, String value, String formatParameter) { + if (value == null || value.isBlank()) { + return; + } + + switch (placeholderName) { + case "SeriesName" -> metadata.setSeriesName(value); + case "Title" -> metadata.setTitle(value); + case "Subtitle" -> metadata.setSubtitle(value); + case "Authors" -> metadata.setAuthors(parseAuthors(value)); + case "SeriesNumber" -> setSeriesNumber(metadata, value); + case "Published" -> setPublishedDate(metadata, value, formatParameter); + case "Publisher" -> metadata.setPublisher(value); + case "Language" -> metadata.setLanguage(value); + case "SeriesTotal" -> setSeriesTotal(metadata, value); + case "ISBN10" -> metadata.setIsbn10(value); + case "ISBN13" -> metadata.setIsbn13(value); + case "ASIN" -> metadata.setAsin(value); + } + } + + private Set parseAuthors(String value) { + String[] parts = value.split("[,;&]"); + Set authors = new LinkedHashSet<>(); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + authors.add(trimmed); + } + } + return authors; + } + + private void setSeriesNumber(BookMetadata metadata, String value) { + try { + metadata.setSeriesNumber(Float.parseFloat(value)); + } catch (NumberFormatException ignored) { + } + } + + private void setPublishedDate(BookMetadata metadata, String value, String dateFormat) { + String detectedFormat = (dateFormat == null || dateFormat.isBlank()) + ? detectDateFormat(value) + : dateFormat; + + if (detectedFormat == null) { + log.warn("Could not detect date format for value: '{}'", value); + return; + } + + try { + if ("yyyy".equals(detectedFormat) || "yy".equals(detectedFormat)) { + int year = Integer.parseInt(value); + if ("yy".equals(detectedFormat) && year < 100) { + year += (year < TWO_DIGIT_YEAR_CUTOFF) ? 2000 : TWO_DIGIT_YEAR_CENTURY_BASE; + } + metadata.setPublishedDate(LocalDate.of(year, 1, 1)); + return; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(detectedFormat); + LocalDate date = LocalDate.parse(value, formatter); + metadata.setPublishedDate(date); + } catch (NumberFormatException e) { + log.warn("Failed to parse year value '{}': {}", value, e.getMessage()); + } catch (DateTimeParseException e) { + log.warn("Failed to parse date '{}' with format '{}': {}", value, detectedFormat, e.getMessage()); + } catch (IllegalArgumentException e) { + log.warn("Invalid date format '{}' for value '{}': {}", detectedFormat, value, e.getMessage()); + } + } + + private String detectDateFormat(String value) { + if (value == null || value.isBlank()) { + return null; + } + + String trimmed = value.trim(); + int length = trimmed.length(); + + if (length == FOUR_DIGIT_YEAR_LENGTH && FOUR_DIGIT_YEAR_PATTERN.matcher(trimmed).matches()) { + return "yyyy"; + } + + if (length == TWO_DIGIT_YEAR_LENGTH && TWO_DIGIT_YEAR_PATTERN.matcher(trimmed).matches()) { + return "yy"; + } + + if (length == COMPACT_DATE_LENGTH && COMPACT_DATE_PATTERN.matcher(trimmed).matches()) { + return "yyyyMMdd"; + } + + Matcher flexibleMatcher = FLEXIBLE_DATE_PATTERN.matcher(trimmed); + if (flexibleMatcher.matches()) { + String separator = flexibleMatcher.group(2); + return determineFlexibleDateFormat(flexibleMatcher, separator); + } + + return null; + } + + private String determineFlexibleDateFormat(Matcher matcher, String separator) { + String part1 = matcher.group(1); + String part2 = matcher.group(3); + String part3 = matcher.group(4); + + int val1, val2, val3; + try { + val1 = Integer.parseInt(part1); + val2 = Integer.parseInt(part2); + val3 = Integer.parseInt(part3); + } catch (NumberFormatException e) { + return null; + } + + String format1, format2, format3; + + if (isYearValue(part1, val1)) { + format1 = buildYearFormat(part1); + if (val2 <= 12 && val3 > 12) { + format2 = buildMonthFormat(part2); + format3 = buildDayFormat(part3); + } else if (val3 <= 12 && val2 > 12) { + format2 = buildDayFormat(part2); + format3 = buildMonthFormat(part3); + } else { + format2 = buildMonthFormat(part2); + format3 = buildDayFormat(part3); + } + } else if (isYearValue(part3, val3)) { + format3 = buildYearFormat(part3); + if (val1 <= 12 && val2 > 12) { + format1 = buildMonthFormat(part1); + format2 = buildDayFormat(part2); + } else if (val2 <= 12 && val1 > 12) { + format1 = buildDayFormat(part1); + format2 = buildMonthFormat(part2); + } else { + format1 = buildDayFormat(part1); + format2 = buildMonthFormat(part2); + } + } else { + format1 = buildDayFormat(part1); + format2 = buildMonthFormat(part2); + format3 = part3.length() == 2 ? "yy" : "y"; + } + + return format1 + separator + format2 + separator + format3; + } + + private boolean isYearValue(String part, int value) { + return part.length() == 4 || value > 31; + } + + private String buildYearFormat(String part) { + return part.length() == 4 ? "yyyy" : "yy"; + } + + private String buildMonthFormat(String part) { + return part.length() == 2 ? "MM" : "M"; + } + + private String buildDayFormat(String part) { + return part.length() == 2 ? "dd" : "d"; + } + + private void setSeriesTotal(BookMetadata metadata, String value) { + try { + metadata.setSeriesTotal(Integer.parseInt(value)); + } catch (NumberFormatException ignored) { + } + } + + private void persistExtractedMetadata(List results, List files) { + Map fileMap = new HashMap<>(); + for (BookdropFileEntity file : files) { + fileMap.put(file.getId(), file); + } + + Set failedFileIds = new HashSet<>(); + + for (BookdropPatternExtractResult.FileExtractionResult result : results) { + if (!result.isSuccess() || result.getExtractedMetadata() == null) { + continue; + } + + BookdropFileEntity file = fileMap.get(result.getFileId()); + if (file == null) { + continue; + } + + try { + BookMetadata currentMetadata = metadataHelper.getCurrentMetadata(file); + BookMetadata extractedMetadata = result.getExtractedMetadata(); + metadataHelper.mergeMetadata(currentMetadata, extractedMetadata); + metadataHelper.updateFetchedMetadata(file, currentMetadata); + + } catch (RuntimeException e) { + log.error("Error persisting extracted metadata for file {} ({}): {}", + file.getId(), file.getFileName(), e.getMessage(), e); + failedFileIds.add(file.getId()); + result.setSuccess(false); + result.setErrorMessage("Failed to save metadata: " + e.getMessage()); + } + } + + List filesToSave = files.stream() + .filter(file -> !failedFileIds.contains(file.getId())) + .toList(); + + if (!filesToSave.isEmpty()) { + bookdropFileRepository.saveAll(filesToSave); + } + } + + private record PlaceholderConfig(String regex, String metadataField) {} + + private record ParsedPattern(Pattern compiledPattern, List placeholderOrder) {} + + private record PlaceholderMatch(int start, int end, String name, String formatParameter) {} + + private record BatchExtractionResult(List results, + int successCount, int failureCount) {} +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/CbxProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/CbxProcessor.java index 817eb4ce..b99e7447 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/CbxProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/CbxProcessor.java @@ -78,8 +78,6 @@ public class CbxProcessor extends AbstractFileProcessor implements BookFileProce try { boolean saved = fileService.saveCoverImages(image, bookEntity.getId()); if (saved) { - bookEntity.getMetadata().setCoverUpdatedOn(Instant.now()); - bookMetadataRepository.save(bookEntity.getMetadata()); return true; } } finally { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java index e710bd2f..c9c9a0fc 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/EpubProcessor.java @@ -80,10 +80,6 @@ public class EpubProcessor extends AbstractFileProcessor implements BookFileProc originalImage.flush(); } - if (saved) { - bookEntity.getMetadata().setCoverUpdatedOn(Instant.now()); - bookMetadataRepository.save(bookEntity.getMetadata()); - } return saved; } catch (Exception e) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/Fb2Processor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/Fb2Processor.java new file mode 100644 index 00000000..0bb51e0c --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/Fb2Processor.java @@ -0,0 +1,140 @@ +package com.adityachandel.booklore.service.fileprocessor; + +import com.adityachandel.booklore.mapper.BookMapper; +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.settings.LibraryFile; +import com.adityachandel.booklore.model.entity.BookEntity; +import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.model.enums.BookFileType; +import com.adityachandel.booklore.repository.BookAdditionalFileRepository; +import com.adityachandel.booklore.repository.BookMetadataRepository; +import com.adityachandel.booklore.repository.BookRepository; +import com.adityachandel.booklore.service.book.BookCreatorService; +import com.adityachandel.booklore.service.metadata.MetadataMatchService; +import com.adityachandel.booklore.service.metadata.extractor.Fb2MetadataExtractor; +import com.adityachandel.booklore.util.FileService; +import com.adityachandel.booklore.util.FileUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.time.Instant; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.adityachandel.booklore.util.FileService.truncate; + +@Slf4j +@Service +public class Fb2Processor extends AbstractFileProcessor implements BookFileProcessor { + + private final Fb2MetadataExtractor fb2MetadataExtractor; + private final BookMetadataRepository bookMetadataRepository; + + public Fb2Processor(BookRepository bookRepository, + BookAdditionalFileRepository bookAdditionalFileRepository, + BookCreatorService bookCreatorService, + BookMapper bookMapper, + FileService fileService, + BookMetadataRepository bookMetadataRepository, + MetadataMatchService metadataMatchService, + Fb2MetadataExtractor fb2MetadataExtractor) { + super(bookRepository, bookAdditionalFileRepository, bookCreatorService, bookMapper, fileService, metadataMatchService); + this.fb2MetadataExtractor = fb2MetadataExtractor; + this.bookMetadataRepository = bookMetadataRepository; + } + + @Override + public BookEntity processNewFile(LibraryFile libraryFile) { + BookEntity bookEntity = bookCreatorService.createShellBook(libraryFile, BookFileType.FB2); + setBookMetadata(bookEntity); + if (generateCover(bookEntity)) { + FileService.setBookCoverPath(bookEntity.getMetadata()); + } + return bookEntity; + } + + @Override + public boolean generateCover(BookEntity bookEntity) { + try { + File fb2File = new File(FileUtils.getBookFullPath(bookEntity)); + byte[] coverData = fb2MetadataExtractor.extractCover(fb2File); + + if (coverData == null || coverData.length == 0) { + log.warn("No cover image found in FB2 '{}'", bookEntity.getFileName()); + return false; + } + + boolean saved = saveCoverImage(coverData, bookEntity.getId()); + return saved; + + } catch (Exception e) { + log.error("Error generating cover for FB2 '{}': {}", bookEntity.getFileName(), e.getMessage(), e); + return false; + } + } + + @Override + public List getSupportedTypes() { + return List.of(BookFileType.FB2); + } + + private void setBookMetadata(BookEntity bookEntity) { + File bookFile = new File(bookEntity.getFullFilePath().toUri()); + BookMetadata fb2Metadata = fb2MetadataExtractor.extractMetadata(bookFile); + if (fb2Metadata == null) return; + + BookMetadataEntity metadata = bookEntity.getMetadata(); + + metadata.setTitle(truncate(fb2Metadata.getTitle(), 1000)); + metadata.setSubtitle(truncate(fb2Metadata.getSubtitle(), 1000)); + metadata.setDescription(truncate(fb2Metadata.getDescription(), 2000)); + metadata.setPublisher(truncate(fb2Metadata.getPublisher(), 1000)); + metadata.setPublishedDate(fb2Metadata.getPublishedDate()); + metadata.setSeriesName(truncate(fb2Metadata.getSeriesName(), 1000)); + metadata.setSeriesNumber(fb2Metadata.getSeriesNumber()); + metadata.setSeriesTotal(fb2Metadata.getSeriesTotal()); + metadata.setIsbn13(truncate(fb2Metadata.getIsbn13(), 64)); + metadata.setIsbn10(truncate(fb2Metadata.getIsbn10(), 64)); + metadata.setPageCount(fb2Metadata.getPageCount()); + + String lang = fb2Metadata.getLanguage(); + metadata.setLanguage(truncate((lang == null || "UND".equalsIgnoreCase(lang)) ? "en" : lang, 1000)); + + metadata.setAsin(truncate(fb2Metadata.getAsin(), 20)); + metadata.setAmazonRating(fb2Metadata.getAmazonRating()); + metadata.setAmazonReviewCount(fb2Metadata.getAmazonReviewCount()); + metadata.setGoodreadsId(truncate(fb2Metadata.getGoodreadsId(), 100)); + metadata.setGoodreadsRating(fb2Metadata.getGoodreadsRating()); + metadata.setGoodreadsReviewCount(fb2Metadata.getGoodreadsReviewCount()); + metadata.setHardcoverId(truncate(fb2Metadata.getHardcoverId(), 100)); + metadata.setHardcoverRating(fb2Metadata.getHardcoverRating()); + metadata.setHardcoverReviewCount(fb2Metadata.getHardcoverReviewCount()); + metadata.setGoogleId(truncate(fb2Metadata.getGoogleId(), 100)); + metadata.setComicvineId(truncate(fb2Metadata.getComicvineId(), 100)); + + bookCreatorService.addAuthorsToBook(fb2Metadata.getAuthors(), bookEntity); + + if (fb2Metadata.getCategories() != null) { + Set validSubjects = fb2Metadata.getCategories().stream() + .filter(s -> s != null && !s.isBlank() && s.length() <= 100 && !s.contains("\n") && !s.contains("\r") && !s.contains(" ")) + .collect(Collectors.toSet()); + bookCreatorService.addCategoriesToBook(validSubjects, bookEntity); + } + } + + private boolean saveCoverImage(byte[] coverData, long bookId) throws Exception { + BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(coverData)); + try { + return fileService.saveCoverImages(originalImage, bookId); + } finally { + if (originalImage != null) { + originalImage.flush(); // Release resources after processing + } + } + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java index f747cedb..d400eac4 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/fileprocessor/PdfProcessor.java @@ -62,10 +62,7 @@ public class PdfProcessor extends AbstractFileProcessor implements BookFileProce @Override public boolean generateCover(BookEntity bookEntity) { try (PDDocument pdf = Loader.loadPDF(new File(FileUtils.getBookFullPath(bookEntity)))) { - boolean saved = generateCoverImageAndSave(bookEntity.getId(), pdf); - bookEntity.getMetadata().setCoverUpdatedOn(Instant.now()); - bookMetadataRepository.save(bookEntity.getMetadata()); - return saved; + return generateCoverImageAndSave(bookEntity.getId(), pdf); } catch (OutOfMemoryError e) { // Note: Catching OOM is generally discouraged, but for batch processing // of potentially large/corrupted PDFs, we prefer graceful degradation diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncService.java new file mode 100644 index 00000000..6a7e96cf --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncService.java @@ -0,0 +1,596 @@ +package com.adityachandel.booklore.service.hardcover; + +import com.adityachandel.booklore.model.dto.settings.MetadataProviderSettings; +import com.adityachandel.booklore.model.entity.BookEntity; +import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.repository.BookRepository; +import com.adityachandel.booklore.service.appsettings.AppSettingService; +import com.adityachandel.booklore.service.metadata.parser.hardcover.GraphQLRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +/** + * Service to sync reading progress to Hardcover. + * Uses the global Hardcover API token from Metadata Provider Settings. + * Sync only activates if the token is configured and Hardcover is enabled. + */ +@Slf4j +@Service +public class HardcoverSyncService { + + private static final String HARDCOVER_API_URL = "https://api.hardcover.app/v1/graphql"; + private static final int STATUS_CURRENTLY_READING = 2; + private static final int STATUS_READ = 3; + + private final RestClient restClient; + private final AppSettingService appSettingService; + private final BookRepository bookRepository; + + @Autowired + public HardcoverSyncService(AppSettingService appSettingService, BookRepository bookRepository) { + this.appSettingService = appSettingService; + this.bookRepository = bookRepository; + this.restClient = RestClient.builder() + .baseUrl(HARDCOVER_API_URL) + .build(); + } + + /** + * Asynchronously sync Kobo reading progress to Hardcover. + * This method is non-blocking and will not fail the calling process if sync fails. + * + * @param bookId The book ID to sync progress for + * @param progressPercent The reading progress as a percentage (0-100) + */ + @Async + @Transactional(readOnly = true) + public void syncProgressToHardcover(Long bookId, Float progressPercent) { + try { + if (!isHardcoverSyncEnabled()) { + log.trace("Hardcover sync skipped: not enabled or no API token configured"); + return; + } + + if (progressPercent == null) { + log.debug("Hardcover sync skipped: no progress to sync"); + return; + } + + // Fetch book fresh within the async context to avoid lazy loading issues + BookEntity book = bookRepository.findById(bookId).orElse(null); + if (book == null) { + log.debug("Hardcover sync skipped: book {} not found", bookId); + return; + } + + BookMetadataEntity metadata = book.getMetadata(); + if (metadata == null) { + log.debug("Hardcover sync skipped: book {} has no metadata", bookId); + return; + } + + // Find the book on Hardcover - use stored ID if available + HardcoverBookInfo hardcoverBook; + if (metadata.getHardcoverBookId() != null) { + // Use the stored numeric book ID directly + hardcoverBook = new HardcoverBookInfo(); + hardcoverBook.bookId = metadata.getHardcoverBookId(); + hardcoverBook.pages = metadata.getPageCount(); + log.debug("Using stored Hardcover book ID: {}", hardcoverBook.bookId); + } else { + // Search by ISBN + hardcoverBook = findHardcoverBook(metadata); + if (hardcoverBook == null) { + log.debug("Hardcover sync skipped: book {} not found on Hardcover", bookId); + return; + } + } + + // Determine the status based on progress + int statusId = progressPercent >= 99.0f ? STATUS_READ : STATUS_CURRENTLY_READING; + + // Calculate progress in pages + int progressPages = 0; + if (hardcoverBook.pages != null && hardcoverBook.pages > 0) { + progressPages = Math.round((progressPercent / 100.0f) * hardcoverBook.pages); + progressPages = Math.max(0, Math.min(hardcoverBook.pages, progressPages)); + } + log.info("Progress calculation: progressPercent={}%, totalPages={}, progressPages={}", + progressPercent, hardcoverBook.pages, progressPages); + + // Step 1: Add/update the book in user's library + Integer userBookId = insertOrGetUserBook(hardcoverBook.bookId, hardcoverBook.editionId, statusId); + if (userBookId == null) { + log.warn("Hardcover sync failed: could not get user_book_id for book {}", bookId); + return; + } + + // Step 2: Create or update the reading progress + boolean success = upsertReadingProgress(userBookId, hardcoverBook.editionId, progressPages); + + if (success) { + log.info("Synced progress to Hardcover: book={}, hardcoverBookId={}, progress={}% ({}pages)", + bookId, hardcoverBook.bookId, Math.round(progressPercent), progressPages); + } + + } catch (Exception e) { + log.error("Failed to sync progress to Hardcover for book {}: {}", + bookId, e.getMessage()); + } + } + + private boolean isHardcoverSyncEnabled() { + MetadataProviderSettings.Hardcover hardcoverSettings = + appSettingService.getAppSettings().getMetadataProviderSettings().getHardcover(); + + if (hardcoverSettings == null) { + return false; + } + + return hardcoverSettings.isEnabled() + && hardcoverSettings.getApiKey() != null + && !hardcoverSettings.getApiKey().isBlank(); + } + + private String getApiToken() { + return appSettingService.getAppSettings() + .getMetadataProviderSettings() + .getHardcover() + .getApiKey(); + } + + /** + * Find a book on Hardcover by ISBN or hardcoverId. + * Returns the numeric book_id, edition_id, and page count. + */ + private HardcoverBookInfo findHardcoverBook(BookMetadataEntity metadata) { + // Try ISBN first + String isbn = metadata.getIsbn13(); + if (isbn == null || isbn.isBlank()) { + isbn = metadata.getIsbn10(); + } + + if (isbn == null || isbn.isBlank()) { + log.debug("No ISBN available for Hardcover lookup"); + return null; + } + + try { + String searchQuery = """ + query SearchBooks($query: String!) { + search(query: $query, query_type: "Book", per_page: 1, page: 1) { + results + } + } + """; + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(searchQuery); + request.setVariables(Map.of("query", isbn)); + + Map response = executeGraphQL(request); + log.debug("Hardcover search response for ISBN {}: {}", isbn, response); + if (response == null) { + return null; + } + + // Navigate the response to get book info + Map data = (Map) response.get("data"); + if (data == null) return null; + + Map search = (Map) data.get("search"); + if (search == null) return null; + + Map results = (Map) search.get("results"); + if (results == null) return null; + + List> hits = (List>) results.get("hits"); + if (hits == null || hits.isEmpty()) return null; + + Map document = (Map) hits.get(0).get("document"); + if (document == null) return null; + + // Extract book info + HardcoverBookInfo info = new HardcoverBookInfo(); + + // The 'id' field contains the numeric book ID + Object idObj = document.get("id"); + if (idObj instanceof String) { + info.bookId = Integer.parseInt((String) idObj); + } else if (idObj instanceof Number) { + info.bookId = ((Number) idObj).intValue(); + } + + // Get page count + Object pagesObj = document.get("pages"); + if (pagesObj instanceof Number) { + info.pages = ((Number) pagesObj).intValue(); + } + + // Try to get default_edition_id from the search results + Object defaultEditionObj = document.get("default_edition_id"); + if (defaultEditionObj instanceof Number) { + info.editionId = ((Number) defaultEditionObj).intValue(); + } else if (defaultEditionObj instanceof String) { + try { + info.editionId = Integer.parseInt((String) defaultEditionObj); + } catch (NumberFormatException e) { + // Ignore + } + } + + // If no default edition, try to look up edition by ISBN + // This also gets the page count from the specific edition + if (info.bookId != null) { + EditionInfo edition = findEditionByIsbn(info.bookId, isbn); + if (edition != null) { + info.editionId = edition.id; + // Prefer edition page count over book page count + if (edition.pages != null && edition.pages > 0) { + info.pages = edition.pages; + } + } + } + + log.info("Found Hardcover book: bookId={}, editionId={}, pages={}", + info.bookId, info.editionId, info.pages); + + return info.bookId != null ? info : null; + + } catch (Exception e) { + log.warn("Failed to search Hardcover by ISBN {}: {}", isbn, e.getMessage()); + return null; + } + } + + /** + * Find an edition by ISBN for a given book. + * This queries Hardcover's editions table to match by ISBN. + */ + private EditionInfo findEditionByIsbn(Integer bookId, String isbn) { + String query = """ + query FindEditionByIsbn($bookId: Int!, $isbn: String!) { + editions(where: { + book_id: {_eq: $bookId}, + _or: [ + {isbn_10: {_eq: $isbn}}, + {isbn_13: {_eq: $isbn}} + ] + }, limit: 1) { + id + pages + } + } + """; + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(query); + request.setVariables(Map.of("bookId", bookId, "isbn", isbn)); + + try { + Map response = executeGraphQL(request); + log.debug("Edition lookup response: {}", response); + if (response == null) return null; + + Map data = (Map) response.get("data"); + if (data == null) return null; + + List> editions = (List>) data.get("editions"); + if (editions == null || editions.isEmpty()) return null; + + Map edition = editions.get(0); + EditionInfo info = new EditionInfo(); + + Object idObj = edition.get("id"); + if (idObj instanceof Number) { + info.id = ((Number) idObj).intValue(); + } + + Object pagesObj = edition.get("pages"); + if (pagesObj instanceof Number) { + info.pages = ((Number) pagesObj).intValue(); + } + + return info.id != null ? info : null; + + } catch (Exception e) { + log.debug("Failed to find edition by ISBN: {}", e.getMessage()); + return null; + } + } + + /** + * Insert a book into the user's library or get existing user_book_id. + */ + private Integer insertOrGetUserBook(Integer bookId, Integer editionId, int statusId) { + String mutation = """ + mutation InsertUserBook($object: UserBookCreateInput!) { + insert_user_book(object: $object) { + user_book { + id + } + error + } + } + """; + + Map bookInput = new java.util.HashMap<>(); + bookInput.put("book_id", bookId); + bookInput.put("status_id", statusId); + bookInput.put("date_added", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); + if (editionId != null) { + bookInput.put("edition_id", editionId); + } + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(mutation); + request.setVariables(Map.of("object", bookInput)); + + try { + Map response = executeGraphQL(request); + log.debug("insert_user_book response: {}", response); + if (response == null) return null; + + Map data = (Map) response.get("data"); + if (data == null) return null; + + Map insertResult = (Map) data.get("insert_user_book"); + if (insertResult == null) return null; + + // Check for error (might mean book already exists) + String error = (String) insertResult.get("error"); + if (error != null && !error.isBlank()) { + log.debug("insert_user_book returned error: {} - book may already exist, trying to find it", error); + return findExistingUserBook(bookId); + } + + Map userBook = (Map) insertResult.get("user_book"); + if (userBook == null) return null; + + Object idObj = userBook.get("id"); + if (idObj instanceof Number) { + return ((Number) idObj).intValue(); + } + + return null; + + } catch (RestClientException e) { + log.warn("Failed to insert user_book: {}", e.getMessage()); + // Try to find existing + return findExistingUserBook(bookId); + } + } + + /** + * Find an existing user_book entry for a book. + */ + private Integer findExistingUserBook(Integer bookId) { + String query = """ + query FindUserBook($bookId: Int!) { + me { + user_books(where: {book_id: {_eq: $bookId}}, limit: 1) { + id + } + } + } + """; + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(query); + request.setVariables(Map.of("bookId", bookId)); + + try { + Map response = executeGraphQL(request); + if (response == null) return null; + + Map data = (Map) response.get("data"); + if (data == null) return null; + + Map me = (Map) data.get("me"); + if (me == null) return null; + + List> userBooks = (List>) me.get("user_books"); + if (userBooks == null || userBooks.isEmpty()) return null; + + Object idObj = userBooks.get(0).get("id"); + if (idObj instanceof Number) { + return ((Number) idObj).intValue(); + } + + return null; + + } catch (RestClientException e) { + log.warn("Failed to find existing user_book: {}", e.getMessage()); + return null; + } + } + + /** + * Create or update reading progress for a user_book. + */ + private boolean upsertReadingProgress(Integer userBookId, Integer editionId, int progressPages) { + log.info("upsertReadingProgress: userBookId={}, editionId={}, progressPages={}", + userBookId, editionId, progressPages); + + // First, try to find existing user_book_read + Integer existingReadId = findExistingUserBookRead(userBookId); + + if (existingReadId != null) { + // Update existing + log.info("Updating existing user_book_read: id={}", existingReadId); + return updateUserBookRead(existingReadId, editionId, progressPages); + } else { + // Create new + log.info("Creating new user_book_read for userBookId={}", userBookId); + return insertUserBookRead(userBookId, editionId, progressPages); + } + } + + private Integer findExistingUserBookRead(Integer userBookId) { + String query = """ + query FindUserBookRead($userBookId: Int!) { + user_book_reads(where: {user_book_id: {_eq: $userBookId}}, limit: 1) { + id + } + } + """; + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(query); + request.setVariables(Map.of("userBookId", userBookId)); + + try { + Map response = executeGraphQL(request); + if (response == null) return null; + + Map data = (Map) response.get("data"); + if (data == null) return null; + + List> reads = (List>) data.get("user_book_reads"); + if (reads == null || reads.isEmpty()) return null; + + Object idObj = reads.get(0).get("id"); + if (idObj instanceof Number) { + return ((Number) idObj).intValue(); + } + + return null; + + } catch (RestClientException e) { + log.warn("Failed to find existing user_book_read: {}", e.getMessage()); + return null; + } + } + + private boolean insertUserBookRead(Integer userBookId, Integer editionId, int progressPages) { + String mutation = """ + mutation InsertUserBookRead($userBookId: Int!, $object: DatesReadInput!) { + insert_user_book_read(user_book_id: $userBookId, user_book_read: $object) { + user_book_read { + id + } + error + } + } + """; + + Map readInput = new java.util.HashMap<>(); + readInput.put("started_at", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); + readInput.put("progress_pages", progressPages); + if (editionId != null) { + readInput.put("edition_id", editionId); + } + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(mutation); + request.setVariables(Map.of( + "userBookId", userBookId, + "object", readInput + )); + + try { + Map response = executeGraphQL(request); + log.info("insert_user_book_read response: {}", response); + if (response == null) return false; + + if (response.containsKey("errors")) { + log.warn("insert_user_book_read returned errors: {}", response.get("errors")); + return false; + } + + return true; + + } catch (RestClientException e) { + log.error("Failed to insert user_book_read: {}", e.getMessage()); + return false; + } + } + + private boolean updateUserBookRead(Integer readId, Integer editionId, int progressPages) { + String mutation = """ + mutation UpdateUserBookRead($id: Int!, $object: DatesReadInput!) { + update_user_book_read(id: $id, object: $object) { + user_book_read { + id + progress + } + error + } + } + """; + + Map readInput = new java.util.HashMap<>(); + readInput.put("progress_pages", progressPages); + if (editionId != null) { + readInput.put("edition_id", editionId); + } + + GraphQLRequest request = new GraphQLRequest(); + request.setQuery(mutation); + request.setVariables(Map.of( + "id", readId, + "object", readInput + )); + + try { + Map response = executeGraphQL(request); + log.debug("update_user_book_read response: {}", response); + if (response == null) return false; + + if (response.containsKey("errors")) { + log.warn("update_user_book_read returned errors: {}", response.get("errors")); + return false; + } + + return true; + + } catch (RestClientException e) { + log.error("Failed to update user_book_read: {}", e.getMessage()); + return false; + } + } + + private Map executeGraphQL(GraphQLRequest request) { + try { + return restClient.post() + .uri("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + getApiToken()) + .body(request) + .retrieve() + .body(Map.class); + } catch (RestClientException e) { + log.error("GraphQL request failed: {}", e.getMessage()); + return null; + } + } + + /** + * Helper class to hold Hardcover book information. + */ + private static class HardcoverBookInfo { + Integer bookId; + Integer editionId; + Integer pages; + } + + /** + * Helper class to hold edition information. + */ + private static class EditionInfo { + Integer id; + Integer pages; + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java index 0215559d..31a3ad51 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java @@ -102,20 +102,20 @@ public class CbxConversionService { * @throws IllegalArgumentException if the file format is not supported * @throws IllegalStateException if no valid images are found in the archive */ - public File convertCbxToEpub(File cbxFile, File tempDir, BookEntity bookEntity) + public File convertCbxToEpub(File cbxFile, File tempDir, BookEntity bookEntity, int compressionPercentage) throws IOException, TemplateException, RarException { validateInputs(cbxFile, tempDir); log.info("Starting CBX to EPUB conversion for: {}", cbxFile.getName()); - File outputFile = executeCbxConversion(cbxFile, tempDir, bookEntity); + File outputFile = executeCbxConversion(cbxFile, tempDir, bookEntity,compressionPercentage); log.info("Successfully converted {} to {} (size: {} bytes)", cbxFile.getName(), outputFile.getName(), outputFile.length()); return outputFile; } - private File executeCbxConversion(File cbxFile, File tempDir, BookEntity bookEntity) + private File executeCbxConversion(File cbxFile, File tempDir, BookEntity bookEntity,int compressionPercentage) throws IOException, TemplateException, RarException { Path epubFilePath = Paths.get(tempDir.getAbsolutePath(), cbxFile.getName() + ".epub"); @@ -136,7 +136,7 @@ public class CbxConversionService { addMetaInfContainer(zipOut); addStylesheet(zipOut); - List contentGroups = addImagesAndPages(zipOut, imagePaths); + List contentGroups = addImagesAndPages(zipOut, imagePaths,compressionPercentage); addContentOpf(zipOut, bookEntity, contentGroups); addTocNcx(zipOut, bookEntity, contentGroups); @@ -340,13 +340,13 @@ public class CbxConversionService { zipOut.closeArchiveEntry(); } - private List addImagesAndPages(ZipArchiveOutputStream zipOut, List imagePaths) + private List addImagesAndPages(ZipArchiveOutputStream zipOut, List imagePaths,int compressionPercentage) throws IOException, TemplateException { List contentGroups = new ArrayList<>(); if (!imagePaths.isEmpty()) { - addImageToZipFromPath(zipOut, COVER_IMAGE_PATH, imagePaths.getFirst()); + addImageToZipFromPath(zipOut, COVER_IMAGE_PATH, imagePaths.getFirst(),compressionPercentage); } for (int i = 0; i < imagePaths.size(); i++) { @@ -358,7 +358,7 @@ public class CbxConversionService { String imagePath = IMAGE_ROOT_PATH + imageFileName; String htmlPath = HTML_ROOT_PATH + htmlFileName; - addImageToZipFromPath(zipOut, imagePath, imageSourcePath); + addImageToZipFromPath(zipOut, imagePath, imageSourcePath,compressionPercentage); String htmlContent = generatePageHtml(imageFileName, i + 1); ZipArchiveEntry htmlEntry = new ZipArchiveEntry(htmlPath); @@ -372,7 +372,7 @@ public class CbxConversionService { return contentGroups; } - private void addImageToZipFromPath(ZipArchiveOutputStream zipOut, String epubImagePath, Path sourceImagePath) + private void addImageToZipFromPath(ZipArchiveOutputStream zipOut, String epubImagePath, Path sourceImagePath,int compressionPercentage) throws IOException { ZipArchiveEntry imageEntry = new ZipArchiveEntry(epubImagePath); zipOut.putArchiveEntry(imageEntry); @@ -385,7 +385,7 @@ public class CbxConversionService { try (InputStream fis = Files.newInputStream(sourceImagePath)) { BufferedImage image = ImageIO.read(fis); if (image != null) { - writeJpegImage(image, zipOut, 0.85f); + writeJpegImage(image, zipOut, compressionPercentage/100f); } else { log.warn("Could not decode image {}, copying raw bytes", sourceImagePath.getFileName()); try (InputStream rawStream = Files.newInputStream(sourceImagePath)) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboInitializationService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboInitializationService.java index b9f3408c..c85c48d0 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboInitializationService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboInitializationService.java @@ -41,6 +41,7 @@ public class KoboInitializationService { objectNode.put("image_host", baseBuilder.build().toUriString()); objectNode.put("image_url_template", koboUrlBuilder.imageUrlTemplate(token)); objectNode.put("image_url_quality_template", koboUrlBuilder.imageUrlQualityTemplate(token)); + objectNode.put("library_sync", koboUrlBuilder.librarySyncUrl(token)); } return ResponseEntity.ok() diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboReadingStateService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboReadingStateService.java index 099236f8..97171740 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboReadingStateService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/KoboReadingStateService.java @@ -16,6 +16,7 @@ import com.adityachandel.booklore.repository.BookRepository; import com.adityachandel.booklore.repository.KoboReadingStateRepository; import com.adityachandel.booklore.repository.UserBookProgressRepository; import com.adityachandel.booklore.repository.UserRepository; +import com.adityachandel.booklore.service.hardcover.HardcoverSyncService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -41,6 +42,7 @@ public class KoboReadingStateService { private final AuthenticationService authenticationService; private final KoboSettingsService koboSettingsService; private final KoboReadingStateBuilder readingStateBuilder; + private final HardcoverSyncService hardcoverSyncService; @Transactional public KoboReadingStateResponse saveReadingState(List readingStates) { @@ -168,6 +170,9 @@ public class KoboReadingStateService { progressRepository.save(progress); log.debug("Synced Kobo progress: bookId={}, progress={}%", bookId, progress.getKoboProgressPercent()); + + // Sync progress to Hardcover asynchronously (if enabled) + hardcoverSyncService.syncProgressToHardcover(book.getId(), progress.getKoboProgressPercent()); } catch (NumberFormatException e) { log.warn("Invalid entitlement ID format: {}", readingState.getEntitlementId()); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java index f526aa90..1ad87ef5 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataService.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -57,6 +58,8 @@ import java.util.stream.Collectors; @AllArgsConstructor public class BookMetadataService { + private static final int BATCH_SIZE = 100; + private final BookRepository bookRepository; private final BookMapper bookMapper; private final BookMetadataMapper bookMetadataMapper; @@ -157,6 +160,76 @@ public class BookMetadataService { return updateCover(bookId, (writer, book) -> writer.replaceCoverImageFromUpload(book, file)); } + public void updateCoverImageFromFileForBooks(Set bookIds, MultipartFile file) { + validateCoverFile(file); + byte[] coverImageBytes = extractBytesFromMultipartFile(file); + List unlockedBooks = getUnlockedBookCoverInfos(bookIds); + + SecurityContextVirtualThread.runWithSecurityContext(() -> + processBulkCoverUpdate(unlockedBooks, coverImageBytes)); + } + + private void validateCoverFile(MultipartFile file) { + if (file.isEmpty()) { + throw ApiError.INVALID_INPUT.createException("Uploaded file is empty"); + } + String contentType = file.getContentType(); + if (contentType == null || (!contentType.toLowerCase().startsWith("image/jpeg") && !contentType.toLowerCase().startsWith("image/png"))) { + throw ApiError.INVALID_INPUT.createException("Only JPEG and PNG files are allowed"); + } + long maxFileSize = 5L * 1024 * 1024; + if (file.getSize() > maxFileSize) { + throw ApiError.FILE_TOO_LARGE.createException(5); + } + } + + private byte[] extractBytesFromMultipartFile(MultipartFile file) { + try { + return file.getBytes(); + } catch (Exception e) { + log.error("Failed to read cover file: {}", e.getMessage()); + throw new RuntimeException("Failed to read cover file", e); + } + } + + private record BookCoverInfo(Long id, String title) {} + + private List getUnlockedBookCoverInfos(Set bookIds) { + return bookQueryService.findAllWithMetadataByIds(bookIds).stream() + .filter(book -> !isCoverLocked(book)) + .map(book -> new BookCoverInfo(book.getId(), book.getMetadata().getTitle())) + .toList(); + } + + private boolean isCoverLocked(BookEntity book) { + return book.getMetadata().getCoverLocked() != null && book.getMetadata().getCoverLocked(); + } + + private void processBulkCoverUpdate(List books, byte[] coverImageBytes) { + try { + int total = books.size(); + notificationService.sendMessage(Topic.LOG, LogNotification.info("Started updating covers for " + total + " selected book(s)")); + + int current = 1; + for (BookCoverInfo bookInfo : books) { + try { + String progress = "(" + current + "/" + total + ") "; + notificationService.sendMessage(Topic.LOG, LogNotification.info(progress + "Updating cover for: " + bookInfo.title())); + fileService.createThumbnailFromBytes(bookInfo.id(), coverImageBytes); + log.info("{}Successfully updated cover for book ID {} ({})", progress, bookInfo.id(), bookInfo.title()); + } catch (Exception e) { + log.error("Failed to update cover for book ID {}: {}", bookInfo.id(), e.getMessage(), e); + } + pauseAfterBatchIfNeeded(current, total); + current++; + } + notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished updating covers for selected books")); + } catch (Exception e) { + log.error("Error during cover update: {}", e.getMessage(), e); + notificationService.sendMessage(Topic.LOG, LogNotification.error("Error occurred during cover update")); + } + } + @Transactional public BookMetadata updateCoverImageFromUrl(Long bookId, String url) { fileService.createThumbnailFromUrl(bookId, url); @@ -190,24 +263,83 @@ public class BookMetadataService { } } + private record BookRegenerationInfo(Long id, String title, BookFileType bookType) {} + + public void regenerateCoversForBooks(Set bookIds) { + List unlockedBooks = getUnlockedBookRegenerationInfos(bookIds); + SecurityContextVirtualThread.runWithSecurityContext(() -> + processBulkCoverRegeneration(unlockedBooks)); + } + + private List getUnlockedBookRegenerationInfos(Set bookIds) { + return bookQueryService.findAllWithMetadataByIds(bookIds).stream() + .filter(book -> !isCoverLocked(book)) + .map(book -> new BookRegenerationInfo(book.getId(), book.getMetadata().getTitle(), book.getBookType())) + .toList(); + } + + private void processBulkCoverRegeneration(List books) { + try { + int total = books.size(); + notificationService.sendMessage(Topic.LOG, LogNotification.info("Started regenerating covers for " + total + " selected book(s)")); + + int current = 1; + for (BookRegenerationInfo bookInfo : books) { + try { + String progress = "(" + current + "/" + total + ") "; + notificationService.sendMessage(Topic.LOG, LogNotification.info(progress + "Regenerating cover for: " + bookInfo.title())); + regenerateCoverForBookId(bookInfo); + log.info("{}Successfully regenerated cover for book ID {} ({})", progress, bookInfo.id(), bookInfo.title()); + } catch (Exception e) { + log.error("Failed to regenerate cover for book ID {}: {}", bookInfo.id(), e.getMessage(), e); + } + pauseAfterBatchIfNeeded(current, total); + current++; + } + notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished regenerating covers for selected books")); + } catch (Exception e) { + log.error("Error during cover regeneration: {}", e.getMessage(), e); + notificationService.sendMessage(Topic.LOG, LogNotification.error("Error occurred during cover regeneration")); + } + } + + private void pauseAfterBatchIfNeeded(int current, int total) { + if (current % BATCH_SIZE == 0 && current < total) { + try { + log.info("Processed {} items, pausing briefly before next batch...", current); + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("Batch pause interrupted"); + } + } + } + + private void regenerateCoverForBookId(BookRegenerationInfo bookInfo) { + bookRepository.findById(bookInfo.id()).ifPresent(book -> { + BookFileProcessor processor = processorRegistry.getProcessorOrThrow(bookInfo.bookType()); + processor.generateCover(book); + }); + } + public void regenerateCovers() { SecurityContextVirtualThread.runWithSecurityContext(() -> { try { List books = bookQueryService.getAllFullBookEntities().stream() - .filter(book -> book.getMetadata().getCoverLocked() == null || !book.getMetadata().getCoverLocked()) + .filter(book -> !isCoverLocked(book)) .toList(); int total = books.size(); notificationService.sendMessage(Topic.LOG, LogNotification.info("Started regenerating covers for " + total + " books")); - int[] current = {1}; + int current = 1; for (BookEntity book : books) { try { - String progress = "(" + current[0] + "/" + total + ") "; + String progress = "(" + current + "/" + total + ") "; regenerateCoverForBook(book, progress); } catch (Exception e) { - log.error("Failed to regenerate cover for book ID {}: {}", book.getId(), e.getMessage()); + log.error("Failed to regenerate cover for book ID {}: {}", book.getId(), e.getMessage(), e); } - current[0]++; + current++; } notificationService.sendMessage(Topic.LOG, LogNotification.info("Finished regenerating covers")); } catch (Exception e) { @@ -219,8 +351,7 @@ public class BookMetadataService { private void regenerateCoverForBook(BookEntity book, String progress) { String title = book.getMetadata().getTitle(); - String message = progress + "Regenerating cover for: " + title; - notificationService.sendMessage(Topic.LOG, LogNotification.info(message)); + notificationService.sendMessage(Topic.LOG, LogNotification.info(progress + "Regenerating cover for: " + title)); BookFileProcessor processor = processorRegistry.getProcessorOrThrow(book.getBookType()); processor.generateCover(book); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java index a5d022bb..fca899ff 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/BookMetadataUpdater.java @@ -160,6 +160,7 @@ public class BookMetadataUpdater { handleFieldUpdate(e.getGoodreadsIdLocked(), clear.isGoodreadsId(), m.getGoodreadsId(), v -> e.setGoodreadsId(nullIfBlank(v)), e::getGoodreadsId, replaceMode); handleFieldUpdate(e.getComicvineIdLocked(), clear.isComicvineId(), m.getComicvineId(), v -> e.setComicvineId(nullIfBlank(v)), e::getComicvineId, replaceMode); handleFieldUpdate(e.getHardcoverIdLocked(), clear.isHardcoverId(), m.getHardcoverId(), v -> e.setHardcoverId(nullIfBlank(v)), e::getHardcoverId, replaceMode); + handleFieldUpdate(e.getHardcoverBookIdLocked(), clear.isHardcoverBookId(), m.getHardcoverBookId(), e::setHardcoverBookId, e::getHardcoverBookId, replaceMode); handleFieldUpdate(e.getGoogleIdLocked(), clear.isGoogleId(), m.getGoogleId(), v -> e.setGoogleId(nullIfBlank(v)), e::getGoogleId, replaceMode); handleFieldUpdate(e.getPageCountLocked(), clear.isPageCount(), m.getPageCount(), e::setPageCount, e::getPageCount, replaceMode); handleFieldUpdate(e.getLanguageLocked(), clear.isLanguage(), m.getLanguage(), v -> e.setLanguage(nullIfBlank(v)), e::getLanguage, replaceMode); @@ -356,7 +357,6 @@ public class BookMetadataUpdater { if (!set) return; if (!StringUtils.hasText(m.getThumbnailUrl()) || isLocalOrPrivateUrl(m.getThumbnailUrl())) return; fileService.createThumbnailFromUrl(bookId, m.getThumbnailUrl()); - e.setCoverUpdatedOn(Instant.now()); } private void updateLocks(BookMetadata m, BookMetadataEntity e) { @@ -375,6 +375,7 @@ public class BookMetadataUpdater { Pair.of(m.getGoodreadsIdLocked(), e::setGoodreadsIdLocked), Pair.of(m.getComicvineIdLocked(), e::setComicvineIdLocked), Pair.of(m.getHardcoverIdLocked(), e::setHardcoverIdLocked), + Pair.of(m.getHardcoverBookIdLocked(), e::setHardcoverBookIdLocked), Pair.of(m.getGoogleIdLocked(), e::setGoogleIdLocked), Pair.of(m.getPageCountLocked(), e::setPageCountLocked), Pair.of(m.getLanguageLocked(), e::setLanguageLocked), diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java index 2fc270a0..288b4bee 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java @@ -482,6 +482,7 @@ public class MetadataRefreshService { if (enabledFields.isHardcoverId()) { if (metadataMap.containsKey(Hardcover)) { metadata.setHardcoverId(metadataMap.get(Hardcover).getHardcoverId()); + metadata.setHardcoverBookId(metadataMap.get(Hardcover).getHardcoverBookId()); } } if (enabledFields.isGoogleId()) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractor.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractor.java new file mode 100644 index 00000000..10a99bef --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractor.java @@ -0,0 +1,367 @@ +package com.adityachandel.booklore.service.metadata.extractor; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; + +@Slf4j +@Component +public class Fb2MetadataExtractor implements FileMetadataExtractor { + + private static final String FB2_NAMESPACE = "http://www.gribuser.ru/xml/fictionbook/2.0"; + private static final Pattern YEAR_PATTERN = Pattern.compile("\\d{4}"); + private static final Pattern ISBN_PATTERN = Pattern.compile("\\d{9}[\\dXx]"); + private static final Pattern KEYWORD_SEPARATOR_PATTERN = Pattern.compile("[,;]"); + private static final Pattern ISBN_CLEANER_PATTERN = Pattern.compile("[^0-9Xx]"); + private static final Pattern ISO_DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}"); + + @Override + public byte[] extractCover(File file) { + try (InputStream inputStream = getInputStream(file)) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.parse(inputStream); + + // Look for cover image in binary elements + NodeList binaries = doc.getElementsByTagNameNS(FB2_NAMESPACE, "binary"); + for (int i = 0; i < binaries.getLength(); i++) { + Element binary = (Element) binaries.item(i); + String id = binary.getAttribute("id"); + + if (id != null && id.toLowerCase().contains("cover")) { + String contentType = binary.getAttribute("content-type"); + if (contentType != null && contentType.startsWith("image/")) { + String base64Data = binary.getTextContent().trim(); + return Base64.getDecoder().decode(base64Data); + } + } + } + + // If no cover found by name, try to find the first referenced image in title-info + Element titleInfo = getFirstElementByTagNameNS(doc, FB2_NAMESPACE, "title-info"); + if (titleInfo != null) { + NodeList coverPages = titleInfo.getElementsByTagNameNS(FB2_NAMESPACE, "coverpage"); + if (coverPages.getLength() > 0) { + Element coverPage = (Element) coverPages.item(0); + NodeList images = coverPage.getElementsByTagNameNS(FB2_NAMESPACE, "image"); + if (images.getLength() > 0) { + Element image = (Element) images.item(0); + String href = image.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href != null && href.startsWith("#")) { + String imageId = href.substring(1); + // Find the binary with this ID + for (int i = 0; i < binaries.getLength(); i++) { + Element binary = (Element) binaries.item(i); + if (imageId.equals(binary.getAttribute("id"))) { + String base64Data = binary.getTextContent().trim(); + return Base64.getDecoder().decode(base64Data); + } + } + } + } + } + } + + return null; + } catch (Exception e) { + log.warn("Failed to extract cover from FB2: {}", file.getName(), e); + return null; + } + } + + @Override + public BookMetadata extractMetadata(File file) { + try (InputStream inputStream = getInputStream(file)) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.parse(inputStream); + + BookMetadata.BookMetadataBuilder metadataBuilder = BookMetadata.builder(); + Set authors = new HashSet<>(); + Set categories = new HashSet<>(); + + // Extract title-info (main metadata section) + Element titleInfo = getFirstElementByTagNameNS(doc, FB2_NAMESPACE, "title-info"); + if (titleInfo != null) { + extractTitleInfo(titleInfo, metadataBuilder, authors, categories); + } + + // Extract publish-info (publisher, year, ISBN) + Element publishInfo = getFirstElementByTagNameNS(doc, FB2_NAMESPACE, "publish-info"); + if (publishInfo != null) { + extractPublishInfo(publishInfo, metadataBuilder); + } + + // Extract document-info (optional metadata) + Element documentInfo = getFirstElementByTagNameNS(doc, FB2_NAMESPACE, "document-info"); + if (documentInfo != null) { + extractDocumentInfo(documentInfo, metadataBuilder); + } + + metadataBuilder.authors(authors); + metadataBuilder.categories(categories); + + return metadataBuilder.build(); + } catch (Exception e) { + log.warn("Failed to extract metadata from FB2: {}", file.getName(), e); + return null; + } + } + + private void extractTitleInfo(Element titleInfo, BookMetadata.BookMetadataBuilder builder, + Set authors, Set categories) { + // Extract genres (categories) + NodeList genres = titleInfo.getElementsByTagNameNS(FB2_NAMESPACE, "genre"); + for (int i = 0; i < genres.getLength(); i++) { + String genre = genres.item(i).getTextContent().trim(); + if (StringUtils.isNotBlank(genre)) { + categories.add(genre); + } + } + + // Extract authors + NodeList authorNodes = titleInfo.getElementsByTagNameNS(FB2_NAMESPACE, "author"); + for (int i = 0; i < authorNodes.getLength(); i++) { + Element author = (Element) authorNodes.item(i); + String authorName = extractPersonName(author); + if (StringUtils.isNotBlank(authorName)) { + authors.add(authorName); + } + } + + // Extract book title + Element bookTitle = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "book-title"); + if (bookTitle != null) { + builder.title(bookTitle.getTextContent().trim()); + } + + // Extract annotation (description) + Element annotation = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "annotation"); + if (annotation != null) { + String description = extractTextFromElement(annotation); + if (StringUtils.isNotBlank(description)) { + builder.description(description); + } + } + + // Extract keywords (additional categories/tags) + Element keywords = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "keywords"); + if (keywords != null) { + String keywordsText = keywords.getTextContent().trim(); + if (StringUtils.isNotBlank(keywordsText)) { + for (String keyword : KEYWORD_SEPARATOR_PATTERN.split(keywordsText)) { + String trimmed = keyword.trim(); + if (StringUtils.isNotBlank(trimmed)) { + categories.add(trimmed); + } + } + } + } + + // Extract date + Element date = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "date"); + if (date != null) { + String dateValue = date.getAttribute("value"); + if (StringUtils.isBlank(dateValue)) { + dateValue = date.getTextContent().trim(); + } + LocalDate publishedDate = parseDate(dateValue); + if (publishedDate != null) { + builder.publishedDate(publishedDate); + } + } + + // Extract language + Element lang = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "lang"); + if (lang != null) { + builder.language(lang.getTextContent().trim()); + } + + // Extract sequence (series information) + Element sequence = getFirstElementByTagNameNS(titleInfo, FB2_NAMESPACE, "sequence"); + if (sequence != null) { + String seriesName = sequence.getAttribute("name"); + if (StringUtils.isNotBlank(seriesName)) { + builder.seriesName(seriesName.trim()); + } + String seriesNumber = sequence.getAttribute("number"); + if (StringUtils.isNotBlank(seriesNumber)) { + try { + builder.seriesNumber(Float.parseFloat(seriesNumber)); + } catch (NumberFormatException e) { + log.debug("Failed to parse series number: {}", seriesNumber); + } + } + } + } + + private void extractPublishInfo(Element publishInfo, BookMetadata.BookMetadataBuilder builder) { + // Extract publisher + Element publisher = getFirstElementByTagNameNS(publishInfo, FB2_NAMESPACE, "publisher"); + if (publisher != null) { + builder.publisher(publisher.getTextContent().trim()); + } + + // Extract publication year + Element year = getFirstElementByTagNameNS(publishInfo, FB2_NAMESPACE, "year"); + if (year != null) { + String yearText = year.getTextContent().trim(); + Matcher matcher = YEAR_PATTERN.matcher(yearText); + if (matcher.find()) { + try { + int yearValue = Integer.parseInt(matcher.group()); + builder.publishedDate(LocalDate.of(yearValue, 1, 1)); + } catch (NumberFormatException e) { + log.debug("Failed to parse year: {}", yearText); + } + } + } + + // Extract ISBN + Element isbn = getFirstElementByTagNameNS(publishInfo, FB2_NAMESPACE, "isbn"); + if (isbn != null) { + String isbnText = ISBN_CLEANER_PATTERN.matcher(isbn.getTextContent().trim()).replaceAll(""); + if (isbnText.length() == 13) { + builder.isbn13(isbnText); + } else if (isbnText.length() == 10) { + builder.isbn10(isbnText); + } else if (ISBN_PATTERN.matcher(isbnText).find()) { + // Extract the first valid ISBN pattern found + Matcher matcher = ISBN_PATTERN.matcher(isbnText); + if (matcher.find()) { + builder.isbn10(matcher.group()); + } + } + } + } + + private void extractDocumentInfo(Element documentInfo, BookMetadata.BookMetadataBuilder builder) { + // Extract document ID (can be used as an identifier) + Element id = getFirstElementByTagNameNS(documentInfo, FB2_NAMESPACE, "id"); + if (id != null) { + // Could potentially map this to a custom identifier field if needed + log.debug("FB2 document ID: {}", id.getTextContent().trim()); + } + } + + private String extractPersonName(Element personElement) { + Element firstName = getFirstElementByTagNameNS(personElement, FB2_NAMESPACE, "first-name"); + Element middleName = getFirstElementByTagNameNS(personElement, FB2_NAMESPACE, "middle-name"); + Element lastName = getFirstElementByTagNameNS(personElement, FB2_NAMESPACE, "last-name"); + Element nickname = getFirstElementByTagNameNS(personElement, FB2_NAMESPACE, "nickname"); + + StringBuilder name = new StringBuilder(64); + + if (firstName != null) { + name.append(firstName.getTextContent().trim()); + } + if (middleName != null) { + if (!name.isEmpty()) name.append(" "); + name.append(middleName.getTextContent().trim()); + } + if (lastName != null) { + if (!name.isEmpty()) name.append(" "); + name.append(lastName.getTextContent().trim()); + } + + // If no name parts found, try nickname + if (name.isEmpty() && nickname != null) { + name.append(nickname.getTextContent().trim()); + } + + return name.toString(); + } + + private String extractTextFromElement(Element element) { + StringBuilder text = new StringBuilder(); + NodeList children = element.getChildNodes(); + + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + text.append(child.getTextContent().trim()).append(" "); + } else if (child.getNodeType() == Node.ELEMENT_NODE) { + Element childElement = (Element) child; + if ("p".equals(childElement.getLocalName())) { + text.append(childElement.getTextContent().trim()).append("\n\n"); + } else { + text.append(extractTextFromElement(childElement)); + } + } + } + + return text.toString().trim(); + } + + private LocalDate parseDate(String dateString) { + if (StringUtils.isBlank(dateString)) { + return null; + } + + try { + // Try parsing ISO date format (YYYY-MM-DD) + if (ISO_DATE_PATTERN.matcher(dateString).matches()) { + return LocalDate.parse(dateString); + } + + // Try extracting year only + Matcher matcher = YEAR_PATTERN.matcher(dateString); + if (matcher.find()) { + int year = Integer.parseInt(matcher.group()); + return LocalDate.of(year, 1, 1); + } + } catch (Exception e) { + log.debug("Failed to parse date: {}", dateString, e); + } + + return null; + } + + private Element getFirstElementByTagNameNS(Node parent, String namespace, String localName) { + NodeList nodes; + if (parent instanceof Document document) { + nodes = document.getElementsByTagNameNS(namespace, localName); + } else if (parent instanceof Element element) { + nodes = element.getElementsByTagNameNS(namespace, localName); + } else { + return null; + } + return nodes.getLength() > 0 ? (Element) nodes.item(0) : null; + } + + private InputStream getInputStream(File file) throws Exception { + FileInputStream fis = new FileInputStream(file); + try { + if (file.getName().toLowerCase().endsWith(".gz")) { + return new GZIPInputStream(fis); + } + return fis; + } catch (Exception e) { + fis.close(); + throw e; + } + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/MetadataExtractorFactory.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/MetadataExtractorFactory.java index 30b6ed05..54ccf587 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/MetadataExtractorFactory.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/extractor/MetadataExtractorFactory.java @@ -15,12 +15,14 @@ public class MetadataExtractorFactory { private final EpubMetadataExtractor epubMetadataExtractor; private final PdfMetadataExtractor pdfMetadataExtractor; private final CbxMetadataExtractor cbxMetadataExtractor; + private final Fb2MetadataExtractor fb2MetadataExtractor; public BookMetadata extractMetadata(BookFileType bookFileType, File file) { return switch (bookFileType) { case PDF -> pdfMetadataExtractor.extractMetadata(file); case EPUB -> epubMetadataExtractor.extractMetadata(file); case CBX -> cbxMetadataExtractor.extractMetadata(file); + case FB2 -> fb2MetadataExtractor.extractMetadata(file); }; } @@ -29,6 +31,7 @@ public class MetadataExtractorFactory { case PDF -> pdfMetadataExtractor.extractMetadata(file); case EPUB -> epubMetadataExtractor.extractMetadata(file); case CBZ, CBR, CB7 -> cbxMetadataExtractor.extractMetadata(file); + case FB2 -> fb2MetadataExtractor.extractMetadata(file); }; } @@ -37,6 +40,7 @@ public class MetadataExtractorFactory { case EPUB -> epubMetadataExtractor.extractCover(file); case PDF -> pdfMetadataExtractor.extractCover(file); case CBZ, CBR, CB7 -> cbxMetadataExtractor.extractCover(file); + case FB2 -> fb2MetadataExtractor.extractCover(file); }; } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java index e3ab0cb4..37654525 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/AmazonBookParser.java @@ -247,7 +247,7 @@ public class AmazonBookParser implements BookParser { return url; } - StringBuilder searchTerm = new StringBuilder(); + StringBuilder searchTerm = new StringBuilder(256); String title = fetchMetadataRequest.getTitle(); if (title != null && !title.isEmpty()) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/ComicvineBookParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/ComicvineBookParser.java index 2808abbe..b07da0a8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/ComicvineBookParser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/ComicvineBookParser.java @@ -77,7 +77,7 @@ public class ComicvineBookParser implements BookParser { HttpRequest request = HttpRequest.newBuilder() .uri(uri) - .header("User-Agent", "Booklore/1.0") + .header("User-Agent", "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)") .GET() .build(); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/DoubanBookParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/DoubanBookParser.java index f5e3f1de..03068643 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/DoubanBookParser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/DoubanBookParser.java @@ -300,7 +300,7 @@ public class DoubanBookParser implements BookParser { } private String buildQueryUrl(FetchMetadataRequest fetchMetadataRequest, Book book) { - StringBuilder searchTerm = new StringBuilder(); + StringBuilder searchTerm = new StringBuilder(256); String title = fetchMetadataRequest.getTitle(); if (title != null && !title.isEmpty()) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/HardcoverParser.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/HardcoverParser.java index 80081202..53621e3b 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/HardcoverParser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/parser/HardcoverParser.java @@ -78,6 +78,14 @@ public class HardcoverParser implements BookParser { .map(doc -> { BookMetadata metadata = new BookMetadata(); metadata.setHardcoverId(doc.getSlug()); + // Set numeric book ID for API operations + if (doc.getId() != null) { + try { + metadata.setHardcoverBookId(Integer.parseInt(doc.getId())); + } catch (NumberFormatException e) { + log.debug("Could not parse Hardcover book ID: {}", doc.getId()); + } + } metadata.setTitle(doc.getTitle()); metadata.setSubtitle(doc.getSubtitle()); metadata.setDescription(doc.getDescription()); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsBookService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsBookService.java index 6a9c05fc..82cc14e8 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsBookService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsBookService.java @@ -7,11 +7,12 @@ import com.adityachandel.booklore.model.dto.*; import com.adityachandel.booklore.model.entity.BookEntity; import com.adityachandel.booklore.model.entity.BookLoreUserEntity; import com.adityachandel.booklore.model.entity.ShelfEntity; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import com.adityachandel.booklore.repository.BookOpdsRepository; import com.adityachandel.booklore.repository.ShelfRepository; import com.adityachandel.booklore.repository.UserRepository; -import com.adityachandel.booklore.service.library.LibraryService; import com.adityachandel.booklore.util.BookUtils; +import com.adityachandel.booklore.service.library.LibraryService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -202,7 +203,7 @@ public class OpdsBookService { if (idPage.isEmpty()) { return new PageImpl<>(List.of(), pageable, 0); } - List books = bookOpdsRepository.findAllWithMetadataByIds(idPage.getContent()); + List books = bookOpdsRepository.findAllWithFullMetadataByIds(idPage.getContent()); return createPageFromEntities(books, idPage, pageable); } @@ -215,7 +216,61 @@ public class OpdsBookService { return new PageImpl<>(List.of(), pageable, 0); } - List books = bookOpdsRepository.findAllWithMetadataByIdsAndLibraryIds(idPage.getContent(), libraryIds); + List books = bookOpdsRepository.findAllWithFullMetadataByIdsAndLibraryIds(idPage.getContent(), libraryIds); + Page booksPage = createPageFromEntities(books, idPage, pageable); + return applyBookFilters(booksPage, userId); + } + + public List getDistinctSeries(Long userId) { + if (userId == null) { + return List.of(); + } + + BookLoreUserEntity entity = userRepository.findById(userId) + .orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(userId)); + BookLoreUser user = bookLoreUserTransformer.toDTO(entity); + + if (user.getPermissions().isAdmin()) { + return bookOpdsRepository.findDistinctSeries(); + } + + Set libraryIds = user.getAssignedLibraries().stream() + .map(Library::getId) + .collect(Collectors.toSet()); + + return bookOpdsRepository.findDistinctSeriesByLibraryIds(libraryIds); + } + + public Page getBooksBySeriesName(Long userId, String seriesName, int page, int size) { + if (userId == null) { + throw ApiError.FORBIDDEN.createException("Authentication required"); + } + + BookLoreUserEntity entity = userRepository.findById(userId) + .orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(userId)); + BookLoreUser user = bookLoreUserTransformer.toDTO(entity); + + Pageable pageable = PageRequest.of(Math.max(page, 0), size); + + if (user.getPermissions().isAdmin()) { + Page idPage = bookOpdsRepository.findBookIdsBySeriesName(seriesName, pageable); + if (idPage.isEmpty()) { + return new PageImpl<>(List.of(), pageable, 0); + } + List books = bookOpdsRepository.findAllWithFullMetadataByIds(idPage.getContent()); + return createPageFromEntities(books, idPage, pageable); + } + + Set libraryIds = user.getAssignedLibraries().stream() + .map(Library::getId) + .collect(Collectors.toSet()); + + Page idPage = bookOpdsRepository.findBookIdsBySeriesNameAndLibraryIds(seriesName, libraryIds, pageable); + if (idPage.isEmpty()) { + return new PageImpl<>(List.of(), pageable, 0); + } + + List books = bookOpdsRepository.findAllWithFullMetadataByIdsAndLibraryIds(idPage.getContent(), libraryIds); Page booksPage = createPageFromEntities(books, idPage, pageable); return applyBookFilters(booksPage, userId); } @@ -346,4 +401,170 @@ public class OpdsBookService { } return dto; } + + public Page applySortOrder(Page booksPage, OpdsSortOrder sortOrder) { + if (sortOrder == null || sortOrder == OpdsSortOrder.RECENT) { + return booksPage; // Already sorted by addedOn DESC from repository + } + + List sortedBooks = new ArrayList<>(booksPage.getContent()); + + switch (sortOrder) { + case TITLE_ASC -> sortedBooks.sort((b1, b2) -> { + String title1 = b1.getMetadata() != null && b1.getMetadata().getTitle() != null + ? b1.getMetadata().getTitle() : ""; + String title2 = b2.getMetadata() != null && b2.getMetadata().getTitle() != null + ? b2.getMetadata().getTitle() : ""; + return title1.compareToIgnoreCase(title2); + }); + case TITLE_DESC -> sortedBooks.sort((b1, b2) -> { + String title1 = b1.getMetadata() != null && b1.getMetadata().getTitle() != null + ? b1.getMetadata().getTitle() : ""; + String title2 = b2.getMetadata() != null && b2.getMetadata().getTitle() != null + ? b2.getMetadata().getTitle() : ""; + return title2.compareToIgnoreCase(title1); + }); + case AUTHOR_ASC -> sortedBooks.sort((b1, b2) -> { + String author1 = getFirstAuthor(b1); + String author2 = getFirstAuthor(b2); + return author1.compareToIgnoreCase(author2); + }); + case AUTHOR_DESC -> sortedBooks.sort((b1, b2) -> { + String author1 = getFirstAuthor(b1); + String author2 = getFirstAuthor(b2); + return author2.compareToIgnoreCase(author1); + }); + case SERIES_ASC -> sortedBooks.sort((b1, b2) -> { + String series1 = getSeriesName(b1); + String series2 = getSeriesName(b2); + boolean hasSeries1 = !series1.isEmpty(); + boolean hasSeries2 = !series2.isEmpty(); + + // Books without series come after books with series + if (!hasSeries1 && !hasSeries2) { + // Both have no series, sort by addedOn descending + return compareByAddedOn(b2, b1); + } + if (!hasSeries1) return 1; + if (!hasSeries2) return -1; + + // Both have series, sort by series name then number + int seriesComp = series1.compareToIgnoreCase(series2); + if (seriesComp != 0) return seriesComp; + return Float.compare(getSeriesNumber(b1), getSeriesNumber(b2)); + }); + case SERIES_DESC -> sortedBooks.sort((b1, b2) -> { + String series1 = getSeriesName(b1); + String series2 = getSeriesName(b2); + boolean hasSeries1 = !series1.isEmpty(); + boolean hasSeries2 = !series2.isEmpty(); + + // Books without series come after books with series + if (!hasSeries1 && !hasSeries2) { + // Both have no series, sort by addedOn descending + return compareByAddedOn(b2, b1); + } + if (!hasSeries1) return 1; + if (!hasSeries2) return -1; + + // Both have series, sort by series name then number + int seriesComp = series2.compareToIgnoreCase(series1); + if (seriesComp != 0) return seriesComp; + return Float.compare(getSeriesNumber(b2), getSeriesNumber(b1)); + }); + case RATING_ASC -> sortedBooks.sort((b1, b2) -> { + Float rating1 = calculateRating(b1); + Float rating2 = calculateRating(b2); + // Books with no rating go to the end + if (rating1 == null && rating2 == null) { + // Both have no rating, fall back to addedOn descending + return compareByAddedOn(b2, b1); + } + if (rating1 == null) return 1; + if (rating2 == null) return -1; + int ratingComp = Float.compare(rating1, rating2); // Ascending order (lowest first) + if (ratingComp != 0) return ratingComp; + // Same rating, fall back to addedOn descending + return compareByAddedOn(b2, b1); + }); + case RATING_DESC -> sortedBooks.sort((b1, b2) -> { + Float rating1 = calculateRating(b1); + Float rating2 = calculateRating(b2); + // Books with no rating go to the end + if (rating1 == null && rating2 == null) { + // Both have no rating, fall back to addedOn descending + return compareByAddedOn(b2, b1); + } + if (rating1 == null) return 1; + if (rating2 == null) return -1; + int ratingComp = Float.compare(rating2, rating1); // Descending order (highest first) + if (ratingComp != 0) return ratingComp; + // Same rating, fall back to addedOn descending + return compareByAddedOn(b2, b1); + }); + } + + return new PageImpl<>(sortedBooks, booksPage.getPageable(), booksPage.getTotalElements()); + } + + private String getFirstAuthor(Book book) { + if (book.getMetadata() != null && book.getMetadata().getAuthors() != null + && !book.getMetadata().getAuthors().isEmpty()) { + return book.getMetadata().getAuthors().iterator().next(); + } + return ""; + } + + private String getSeriesName(Book book) { + if (book.getMetadata() != null && book.getMetadata().getSeriesName() != null) { + return book.getMetadata().getSeriesName(); + } + return ""; + } + + private Float getSeriesNumber(Book book) { + if (book.getMetadata() != null && book.getMetadata().getSeriesNumber() != null) { + return book.getMetadata().getSeriesNumber(); + } + return Float.MAX_VALUE; + } + + private int compareByAddedOn(Book b1, Book b2) { + if (b1.getAddedOn() == null && b2.getAddedOn() == null) return 0; + if (b1.getAddedOn() == null) return 1; + if (b2.getAddedOn() == null) return -1; + return b1.getAddedOn().compareTo(b2.getAddedOn()); + } + + private Float calculateRating(Book book) { + if (book.getMetadata() == null) { + return null; + } + + Double hardcoverRating = book.getMetadata().getHardcoverRating(); + Double amazonRating = book.getMetadata().getAmazonRating(); + Double goodreadsRating = book.getMetadata().getGoodreadsRating(); + + double sum = 0; + int count = 0; + + if (hardcoverRating != null && hardcoverRating > 0) { + sum += hardcoverRating; + count++; + } + if (amazonRating != null && amazonRating > 0) { + sum += amazonRating; + count++; + } + if (goodreadsRating != null && goodreadsRating > 0) { + sum += goodreadsRating; + count++; + } + + if (count == 0) { + return null; + } + + return (float) (sum / count); + } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsFeedService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsFeedService.java index 80f2ed43..18e80ea9 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsFeedService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsFeedService.java @@ -5,6 +5,7 @@ import com.adityachandel.booklore.config.security.userdetails.OpdsUserDetails; import com.adityachandel.booklore.model.dto.Book; import com.adityachandel.booklore.model.dto.Library; import com.adityachandel.booklore.model.dto.MagicShelf; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import com.adityachandel.booklore.service.MagicShelfService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -100,6 +101,16 @@ public class OpdsFeedService { """.formatted(now())); + feed.append(""" + + Series + urn:booklore:navigation:series + %s + + Browse books by series + + """.formatted(now())); + feed.append(""" Surprise Me @@ -270,28 +281,72 @@ public class OpdsFeedService { return feed.toString(); } + public String generateSeriesNavigation(HttpServletRequest request) { + Long userId = getUserId(); + List seriesList = opdsBookService.getDistinctSeries(userId); + + var feed = new StringBuilder(""" + + + urn:booklore:navigation:series + Series + %s + + + + """.formatted(now())); + + for (String series : seriesList) { + feed.append(""" + + %s + urn:booklore:series:%s + %s + + Books in the %s series + + """.formatted( + escapeXml(series), + escapeXml(series), + now(), + escapeXml("/api/v1/opds/catalog?series=" + java.net.URLEncoder.encode(series, java.nio.charset.StandardCharsets.UTF_8)), + escapeXml(series) + )); + } + + feed.append(""); + return feed.toString(); + } + public String generateCatalogFeed(HttpServletRequest request) { Long libraryId = parseLongParam(request, "libraryId", null); Long shelfId = parseLongParam(request, "shelfId", null); Long magicShelfId = parseLongParam(request, "magicShelfId", null); String query = request.getParameter("q"); String author = request.getParameter("author"); + String series = request.getParameter("series"); int page = Math.max(1, parseLongParam(request, "page", 1L).intValue()); int size = Math.min(parseLongParam(request, "size", (long) DEFAULT_PAGE_SIZE).intValue(), MAX_PAGE_SIZE); Long userId = getUserId(); + OpdsSortOrder sortOrder = getSortOrder(); Page booksPage; if (magicShelfId != null) { booksPage = magicShelfBookService.getBooksByMagicShelfId(userId, magicShelfId, page - 1, size); } else if (author != null && !author.isBlank()) { booksPage = opdsBookService.getBooksByAuthorName(userId, author, page - 1, size); + } else if (series != null && !series.isBlank()) { + booksPage = opdsBookService.getBooksBySeriesName(userId, series, page - 1, size); } else { booksPage = opdsBookService.getBooksPage(userId, query, libraryId, shelfId, page - 1, size); } - String feedTitle = determineFeedTitle(libraryId, shelfId, magicShelfId, author); - String feedId = determineFeedId(libraryId, shelfId, magicShelfId, author); + // Apply user's preferred sort order + booksPage = opdsBookService.applySortOrder(booksPage, sortOrder); + + String feedTitle = determineFeedTitle(libraryId, shelfId, magicShelfId, author, series); + String feedId = determineFeedId(libraryId, shelfId, magicShelfId, author, series); var feed = new StringBuilder(""" @@ -325,10 +380,14 @@ public class OpdsFeedService { public String generateRecentFeed(HttpServletRequest request) { Long userId = getUserId(); + OpdsSortOrder sortOrder = getSortOrder(); int page = Math.max(1, parseLongParam(request, "page", 1L).intValue()); int size = Math.min(parseLongParam(request, "size", (long) DEFAULT_PAGE_SIZE).intValue(), MAX_PAGE_SIZE); Page booksPage = opdsBookService.getRecentBooksPage(userId, page - 1, size); + + // Apply user's preferred sort order + booksPage = opdsBookService.applySortOrder(booksPage, sortOrder); var feed = new StringBuilder(""" @@ -502,7 +561,7 @@ public class OpdsFeedService { } } - private String determineFeedTitle(Long libraryId, Long shelfId, Long magicShelfId, String author) { + private String determineFeedTitle(Long libraryId, Long shelfId, Long magicShelfId, String author, String series) { if (magicShelfId != null) { return magicShelfBookService.getMagicShelfName(magicShelfId); } @@ -515,10 +574,13 @@ public class OpdsFeedService { if (author != null && !author.isBlank()) { return "Books by " + author; } + if (series != null && !series.isBlank()) { + return series + " series"; + } return "Booklore Catalog"; } - private String determineFeedId(Long libraryId, Long shelfId, Long magicShelfId, String author) { + private String determineFeedId(Long libraryId, Long shelfId, Long magicShelfId, String author, String series) { if (magicShelfId != null) { return "urn:booklore:magic-shelf:" + magicShelfId; } @@ -531,6 +593,9 @@ public class OpdsFeedService { if (author != null && !author.isBlank()) { return "urn:booklore:author:" + author; } + if (series != null && !series.isBlank()) { + return "urn:booklore:series:" + series; + } return "urn:booklore:catalog"; } @@ -574,4 +639,11 @@ public class OpdsFeedService { ? details.getOpdsUserV2().getUserId() : null; } + + private OpdsSortOrder getSortOrder() { + OpdsUserDetails details = authenticationService.getOpdsUser(); + return details != null && details.getOpdsUserV2() != null && details.getOpdsUserV2().getSortOrder() != null + ? details.getOpdsUserV2().getSortOrder() + : OpdsSortOrder.RECENT; + } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsUserV2Service.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsUserV2Service.java index 222f88ea..063ad555 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsUserV2Service.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/opds/OpdsUserV2Service.java @@ -5,6 +5,7 @@ import com.adityachandel.booklore.mapper.OpdsUserV2Mapper; import com.adityachandel.booklore.model.dto.BookLoreUser; import com.adityachandel.booklore.model.dto.OpdsUserV2; import com.adityachandel.booklore.model.dto.request.OpdsUserV2CreateRequest; +import com.adityachandel.booklore.model.dto.request.OpdsUserV2UpdateRequest; import com.adityachandel.booklore.model.entity.BookLoreUserEntity; import com.adityachandel.booklore.model.entity.OpdsUserV2Entity; import com.adityachandel.booklore.repository.OpdsUserV2Repository; @@ -45,6 +46,7 @@ public class OpdsUserV2Service { .user(userEntity) .username(request.getUsername()) .passwordHash(passwordEncoder.encode(request.getPassword())) + .sortOrder(request.getSortOrder() != null ? request.getSortOrder() : com.adityachandel.booklore.model.enums.OpdsSortOrder.RECENT) .build(); return mapper.toDto(opdsUserV2Repository.save(opdsUserV2)); @@ -64,4 +66,17 @@ public class OpdsUserV2Service { } opdsUserV2Repository.delete(user); } + + public OpdsUserV2 updateOpdsUser(Long userId, OpdsUserV2UpdateRequest request) { + BookLoreUser bookLoreUser = authenticationService.getAuthenticatedUser(); + OpdsUserV2Entity user = opdsUserV2Repository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId)); + + if (!user.getUser().getId().equals(bookLoreUser.getId())) { + throw new AccessDeniedException("You are not allowed to update this user"); + } + + user.setSortOrder(request.sortOrder()); + return mapper.toDto(opdsUserV2Repository.save(user)); + } } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/watcher/BookFilePersistenceService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/watcher/BookFilePersistenceService.java index 4216defd..cd3f8181 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/watcher/BookFilePersistenceService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/watcher/BookFilePersistenceService.java @@ -76,6 +76,9 @@ public class BookFilePersistenceService { @Transactional public int markAllBooksUnderPathAsDeleted(long libraryPathId, String relativeFolderPath) { + if (relativeFolderPath == null) { + throw new IllegalArgumentException("relativeFolderPath cannot be null"); + } String normalizedPrefix = relativeFolderPath.endsWith("/") ? relativeFolderPath : (relativeFolderPath + "/"); List books = bookRepository.findAllByLibraryPathIdAndFileSubPathStartingWith(libraryPathId, normalizedPrefix); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/BookUtils.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/BookUtils.java index a4b23fdf..3219bb6c 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/BookUtils.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/BookUtils.java @@ -12,11 +12,12 @@ public class BookUtils { private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile("[!@$%^&*_=|~`<>?/\"]"); private static final Pattern PARENTHESES_WITH_OPTIONAL_SPACE_PATTERN = Pattern.compile("\\s?\\(.*?\\)"); + private static final Pattern DIACRITICAL_MARKS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); public static String buildSearchText(BookMetadataEntity e) { if (e == null) return null; - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(256); if (e.getTitle() != null) sb.append(e.getTitle()).append(" "); if (e.getSubtitle() != null) sb.append(e.getSubtitle()).append(" "); if (e.getSeriesName() != null) sb.append(e.getSeriesName()).append(" "); @@ -41,7 +42,7 @@ public class BookUtils { return null; } String s = java.text.Normalizer.normalize(term, java.text.Normalizer.Form.NFD); - s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + s = DIACRITICAL_MARKS_PATTERN.matcher(s).replaceAll(""); s = s.replace("รธ", "o").replace("ร˜", "O") .replace("ล‚", "l").replace("ล", "L") .replace("รฆ", "ae").replace("ร†", "AE") @@ -82,7 +83,7 @@ public class BookUtils { if (s.length() > 60) { String[] words = WHITESPACE_PATTERN.split(s); if (words.length > 1) { - StringBuilder truncated = new StringBuilder(); + StringBuilder truncated = new StringBuilder(64); for (String word : words) { if (truncated.length() + word.length() + 1 > 60) break; if (!truncated.isEmpty()) truncated.append(" "); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/FileService.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/FileService.java index bfbf7d33..92812f01 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/FileService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/FileService.java @@ -2,7 +2,10 @@ package com.adityachandel.booklore.util; import com.adityachandel.booklore.config.AppProperties; import com.adityachandel.booklore.exception.ApiError; +import com.adityachandel.booklore.model.dto.settings.CoverCroppingSettings; import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.repository.BookMetadataRepository; +import com.adityachandel.booklore.service.appsettings.AppSettingService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; @@ -38,6 +41,12 @@ public class FileService { private final AppProperties appProperties; private final RestTemplate restTemplate; + private final AppSettingService appSettingService; + private final BookMetadataRepository bookMetadataRepository; + + private static final double TARGET_COVER_ASPECT_RATIO = 1.5; + private static final int SMART_CROP_COLOR_TOLERANCE = 30; + private static final double SMART_CROP_MARGIN_PERCENT = 0.02; // @formatter:off private static final String IMAGES_DIR = "images"; @@ -168,7 +177,7 @@ public class FileService { public BufferedImage downloadImageFromUrl(String imageUrl) throws IOException { try { HttpHeaders headers = new HttpHeaders(); - headers.set(HttpHeaders.USER_AGENT, "BookLore/1.0 (Metadata Fetcher)"); + headers.set(HttpHeaders.USER_AGENT, "BookLore/1.0 (Book and Comic Metadata Fetcher; +https://github.com/booklore-app/booklore)"); headers.set(HttpHeaders.ACCEPT, "image/*"); HttpEntity entity = new HttpEntity<>(headers); @@ -224,6 +233,27 @@ public class FileService { } } + public void createThumbnailFromBytes(long bookId, byte[] imageBytes) { + try { + BufferedImage originalImage; + try (InputStream inputStream = new java.io.ByteArrayInputStream(imageBytes)) { + originalImage = ImageIO.read(inputStream); + } + if (originalImage == null) { + throw ApiError.IMAGE_NOT_FOUND.createException(); + } + boolean success = saveCoverImages(originalImage, bookId); + if (!success) { + throw ApiError.FILE_READ_ERROR.createException("Failed to save cover images"); + } + originalImage.flush(); + log.info("Cover images created and saved from bytes for book ID: {}", bookId); + } catch (Exception e) { + log.error("An error occurred while creating thumbnail from bytes: {}", e.getMessage(), e); + throw ApiError.FILE_READ_ERROR.createException(e.getMessage()); + } + } + public void createThumbnailFromUrl(long bookId, String imageUrl) { try { BufferedImage originalImage = downloadImageFromUrl(imageUrl); @@ -241,6 +271,7 @@ public class FileService { public boolean saveCoverImages(BufferedImage coverImage, long bookId) throws IOException { BufferedImage rgbImage = null; + BufferedImage cropped = null; BufferedImage resized = null; BufferedImage thumb = null; try { @@ -260,6 +291,12 @@ public class FileService { g.dispose(); // Note: coverImage is not flushed here - caller is responsible for its lifecycle + cropped = applyCoverCropping(rgbImage); + if (cropped != rgbImage) { + rgbImage.flush(); + rgbImage = cropped; + } + // Resize original image if too large to prevent OOM double scale = Math.min( (double) MAX_ORIGINAL_WIDTH / rgbImage.getWidth(), @@ -278,13 +315,19 @@ public class FileService { File thumbnailFile = new File(folder, THUMBNAIL_FILENAME); boolean thumbnailSaved = ImageIO.write(thumb, IMAGE_FORMAT, thumbnailFile); + if (originalSaved && thumbnailSaved) { + bookMetadataRepository.updateCoverTimestamp(bookId, Instant.now()); + } return originalSaved && thumbnailSaved; } finally { // Cleanup resources created within this method - // Note: resized may equal rgbImage after reassignment, avoid double-flush + // Note: cropped/resized may equal rgbImage after reassignment, avoid double-flush if (rgbImage != null) { rgbImage.flush(); } + if (cropped != null && cropped != rgbImage) { + cropped.flush(); + } if (resized != null && resized != rgbImage) { resized.flush(); } @@ -294,6 +337,110 @@ public class FileService { } } + private BufferedImage applyCoverCropping(BufferedImage image) { + CoverCroppingSettings settings = appSettingService.getAppSettings().getCoverCroppingSettings(); + if (settings == null) { + return image; + } + + int width = image.getWidth(); + int height = image.getHeight(); + double heightToWidthRatio = (double) height / width; + double widthToHeightRatio = (double) width / height; + double threshold = settings.getAspectRatioThreshold(); + boolean smartCrop = settings.isSmartCroppingEnabled(); + + boolean isExtremelyTall = settings.isVerticalCroppingEnabled() && heightToWidthRatio > threshold; + if (isExtremelyTall) { + int croppedHeight = (int) (width * TARGET_COVER_ASPECT_RATIO); + log.debug("Cropping tall image: {}x{} (ratio {}) -> {}x{}, smartCrop={}", + width, height, String.format("%.2f", heightToWidthRatio), width, croppedHeight, smartCrop); + return cropFromTop(image, width, croppedHeight, smartCrop); + } + + boolean isExtremelyWide = settings.isHorizontalCroppingEnabled() && widthToHeightRatio > threshold; + if (isExtremelyWide) { + int croppedWidth = (int) (height / TARGET_COVER_ASPECT_RATIO); + log.debug("Cropping wide image: {}x{} (ratio {}) -> {}x{}, smartCrop={}", + width, height, String.format("%.2f", widthToHeightRatio), croppedWidth, height, smartCrop); + return cropFromLeft(image, croppedWidth, height, smartCrop); + } + + return image; + } + + private BufferedImage cropFromTop(BufferedImage image, int targetWidth, int targetHeight, boolean smartCrop) { + int startY = 0; + if (smartCrop) { + int contentStartY = findContentStartY(image); + int margin = (int) (targetHeight * SMART_CROP_MARGIN_PERCENT); + startY = Math.max(0, contentStartY - margin); + + int maxStartY = image.getHeight() - targetHeight; + startY = Math.min(startY, maxStartY); + } + return image.getSubimage(0, startY, targetWidth, targetHeight); + } + + private BufferedImage cropFromLeft(BufferedImage image, int targetWidth, int targetHeight, boolean smartCrop) { + int startX = 0; + if (smartCrop) { + int contentStartX = findContentStartX(image); + int margin = (int) (targetWidth * SMART_CROP_MARGIN_PERCENT); + startX = Math.max(0, contentStartX - margin); + + int maxStartX = image.getWidth() - targetWidth; + startX = Math.min(startX, maxStartX); + } + return image.getSubimage(startX, 0, targetWidth, targetHeight); + } + + private int findContentStartY(BufferedImage image) { + for (int y = 0; y < image.getHeight(); y++) { + if (!isRowUniformColor(image, y)) { + return y; + } + } + return 0; + } + + private int findContentStartX(BufferedImage image) { + for (int x = 0; x < image.getWidth(); x++) { + if (!isColumnUniformColor(image, x)) { + return x; + } + } + return 0; + } + + private boolean isRowUniformColor(BufferedImage image, int y) { + int firstPixel = image.getRGB(0, y); + for (int x = 1; x < image.getWidth(); x++) { + if (!colorsAreSimilar(firstPixel, image.getRGB(x, y))) { + return false; + } + } + return true; + } + + private boolean isColumnUniformColor(BufferedImage image, int x) { + int firstPixel = image.getRGB(x, 0); + for (int y = 1; y < image.getHeight(); y++) { + if (!colorsAreSimilar(firstPixel, image.getRGB(x, y))) { + return false; + } + } + return true; + } + + private boolean colorsAreSimilar(int rgb1, int rgb2) { + int r1 = (rgb1 >> 16) & 0xFF, g1 = (rgb1 >> 8) & 0xFF, b1 = rgb1 & 0xFF; + int r2 = (rgb2 >> 16) & 0xFF, g2 = (rgb2 >> 8) & 0xFF, b2 = rgb2 & 0xFF; + return Math.abs(r1 - r2) <= SMART_CROP_COLOR_TOLERANCE + && Math.abs(g1 - g2) <= SMART_CROP_COLOR_TOLERANCE + && Math.abs(b1 - b2) <= SMART_CROP_COLOR_TOLERANCE; + } + public static void setBookCoverPath(BookMetadataEntity bookMetadataEntity) { bookMetadataEntity.setCoverUpdatedOn(Instant.now()); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java index 870cf5ce..b4f65b10 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/MetadataChangeDetector.java @@ -35,6 +35,7 @@ public class MetadataChangeDetector { compare(changes, "goodreadsId", clear.isGoodreadsId(), newMeta.getGoodreadsId(), existingMeta.getGoodreadsId(), () -> !isTrue(existingMeta.getGoodreadsIdLocked()), newMeta.getGoodreadsIdLocked(), existingMeta.getGoodreadsIdLocked()); compare(changes, "comicvineId", clear.isComicvineId(), newMeta.getComicvineId(), existingMeta.getComicvineId(), () -> !isTrue(existingMeta.getComicvineIdLocked()), newMeta.getComicvineIdLocked(), existingMeta.getComicvineIdLocked()); compare(changes, "hardcoverId", clear.isHardcoverId(), newMeta.getHardcoverId(), existingMeta.getHardcoverId(), () -> !isTrue(existingMeta.getHardcoverIdLocked()), newMeta.getHardcoverIdLocked(), existingMeta.getHardcoverIdLocked()); + compare(changes, "hardcoverBookId", clear.isHardcoverBookId(), newMeta.getHardcoverBookId(), existingMeta.getHardcoverBookId(), () -> !isTrue(existingMeta.getHardcoverBookIdLocked()), newMeta.getHardcoverBookIdLocked(), existingMeta.getHardcoverBookIdLocked()); compare(changes, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked()), newMeta.getGoogleIdLocked(), existingMeta.getGoogleIdLocked()); compare(changes, "pageCount", clear.isPageCount(), newMeta.getPageCount(), existingMeta.getPageCount(), () -> !isTrue(existingMeta.getPageCountLocked()), newMeta.getPageCountLocked(), existingMeta.getPageCountLocked()); compare(changes, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked()), newMeta.getLanguageLocked(), existingMeta.getLanguageLocked()); @@ -75,6 +76,7 @@ public class MetadataChangeDetector { compareValue(diffs, "goodreadsId", clear.isGoodreadsId(), newMeta.getGoodreadsId(), existingMeta.getGoodreadsId(), () -> !isTrue(existingMeta.getGoodreadsIdLocked())); compareValue(diffs, "comicvineId", clear.isComicvineId(), newMeta.getComicvineId(), existingMeta.getComicvineId(), () -> !isTrue(existingMeta.getComicvineIdLocked())); compareValue(diffs, "hardcoverId", clear.isHardcoverId(), newMeta.getHardcoverId(), existingMeta.getHardcoverId(), () -> !isTrue(existingMeta.getHardcoverIdLocked())); + compareValue(diffs, "hardcoverBookId", clear.isHardcoverBookId(), newMeta.getHardcoverBookId(), existingMeta.getHardcoverBookId(), () -> !isTrue(existingMeta.getHardcoverBookIdLocked())); compareValue(diffs, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked())); compareValue(diffs, "pageCount", clear.isPageCount(), newMeta.getPageCount(), existingMeta.getPageCount(), () -> !isTrue(existingMeta.getPageCountLocked())); compareValue(diffs, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked())); @@ -107,6 +109,7 @@ public class MetadataChangeDetector { compareValue(diffs, "goodreadsId", clear.isGoodreadsId(), newMeta.getGoodreadsId(), existingMeta.getGoodreadsId(), () -> !isTrue(existingMeta.getGoodreadsIdLocked())); compareValue(diffs, "comicvineId", clear.isComicvineId(), newMeta.getComicvineId(), existingMeta.getComicvineId(), () -> !isTrue(existingMeta.getComicvineIdLocked())); compareValue(diffs, "hardcoverId", clear.isHardcoverId(), newMeta.getHardcoverId(), existingMeta.getHardcoverId(), () -> !isTrue(existingMeta.getHardcoverIdLocked())); + compareValue(diffs, "hardcoverBookId", clear.isHardcoverBookId(), newMeta.getHardcoverBookId(), existingMeta.getHardcoverBookId(), () -> !isTrue(existingMeta.getHardcoverBookIdLocked())); compareValue(diffs, "googleId", clear.isGoogleId(), newMeta.getGoogleId(), existingMeta.getGoogleId(), () -> !isTrue(existingMeta.getGoogleIdLocked())); compareValue(diffs, "language", clear.isLanguage(), newMeta.getLanguage(), existingMeta.getLanguage(), () -> !isTrue(existingMeta.getLanguageLocked())); compareValue(diffs, "authors", clear.isAuthors(), newMeta.getAuthors(), toNameSet(existingMeta.getAuthors()), () -> !isTrue(existingMeta.getAuthorsLocked())); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/PathPatternResolver.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/PathPatternResolver.java index 5c55ea03..237ded4a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/PathPatternResolver.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/PathPatternResolver.java @@ -28,7 +28,6 @@ public class PathPatternResolver { private final int SUFFIX_BYTES = TRUNCATION_SUFFIX.getBytes(StandardCharsets.UTF_8).length; private final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); - private final Pattern FILE_EXTENSION_PATTERN = Pattern.compile(".*\\.[a-zA-Z0-9]+$"); private final Pattern CONTROL_CHARACTER_PATTERN = Pattern.compile("\\p{Cntrl}"); private final Pattern INVALID_CHARS_PATTERN = Pattern.compile("[\\\\/:*?\"<>|]"); private final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(.*?)}"); @@ -129,7 +128,7 @@ public class PathPatternResolver { // Handle optional blocks enclosed in <...> Pattern optionalBlockPattern = Pattern.compile("<([^<>]*)>"); Matcher matcher = optionalBlockPattern.matcher(pattern); - StringBuilder resolved = new StringBuilder(); + StringBuilder resolved = new StringBuilder(1024); while (matcher.find()) { String block = matcher.group(1); @@ -162,7 +161,7 @@ public class PathPatternResolver { // Replace known placeholders with values, preserve unknown ones Matcher placeholderMatcher = PLACEHOLDER_PATTERN.matcher(result); - StringBuilder finalResult = new StringBuilder(); + StringBuilder finalResult = new StringBuilder(1024); while (placeholderMatcher.find()) { String key = placeholderMatcher.group(1); @@ -178,14 +177,16 @@ public class PathPatternResolver { result = finalResult.toString(); + boolean usedFallbackFilename = false; if (result.isBlank()) { result = values.getOrDefault("currentFilename", "untitled"); + usedFallbackFilename = true; } - boolean hasExtension = FILE_EXTENSION_PATTERN.matcher(result).matches(); - boolean explicitlySetExtension = pattern.contains("{extension}"); - - if (!explicitlySetExtension && !hasExtension && !extension.isBlank()) { + boolean patternIncludesExtension = pattern.contains("{extension}"); + boolean patternIncludesFullFilename = pattern.contains("{currentFilename}"); + + if (!usedFallbackFilename && !patternIncludesExtension && !patternIncludesFullFilename && !extension.isBlank()) { result += "." + extension; } @@ -209,7 +210,7 @@ public class PathPatternResolver { } String[] authorArray = COMMA_SPACE_PATTERN.split(authors); - StringBuilder result = new StringBuilder(); + StringBuilder result = new StringBuilder(256); int currentBytes = 0; int truncationLimit = MAX_AUTHOR_BYTES - SUFFIX_BYTES; @@ -264,7 +265,7 @@ public class PathPatternResolver { private String validateFinalPath(String path) { String[] components = SLASH_PATTERN.split(path); - StringBuilder result = new StringBuilder(); + StringBuilder result = new StringBuilder(512); for (int i = 0; i < components.length; i++) { String component = components[i]; @@ -276,7 +277,7 @@ public class PathPatternResolver { if (component.getBytes(StandardCharsets.UTF_8).length > MAX_FILESYSTEM_COMPONENT_BYTES) { component = truncatePathComponent(component, MAX_FILESYSTEM_COMPONENT_BYTES); } - while (component.endsWith(".")) { + while (component != null && !component.isEmpty() && component.endsWith(".")) { component = component.substring(0, component.length() - 1); } } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/util/kobo/KoboUrlBuilder.java b/booklore-api/src/main/java/com/adityachandel/booklore/util/kobo/KoboUrlBuilder.java index ba7f04eb..52c63dba 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/util/kobo/KoboUrlBuilder.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/util/kobo/KoboUrlBuilder.java @@ -70,4 +70,11 @@ public class KoboUrlBuilder { .build() .toUriString(); } + + public String librarySyncUrl(String token) { + return baseBuilder() + .pathSegment("api", "kobo", token, "v1", "library", "sync") + .build() + .toUriString(); + } } \ No newline at end of file diff --git a/booklore-api/src/main/resources/db/migration/V73__Add_sort_order_to_opds_user_v2.sql b/booklore-api/src/main/resources/db/migration/V73__Add_sort_order_to_opds_user_v2.sql new file mode 100644 index 00000000..e981c7f9 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V73__Add_sort_order_to_opds_user_v2.sql @@ -0,0 +1,2 @@ +-- Add sort_order column to opds_user_v2 table +ALTER TABLE opds_user_v2 ADD COLUMN sort_order VARCHAR(20) NOT NULL DEFAULT 'RECENT'; diff --git a/booklore-api/src/main/resources/db/migration/V74__Add_hardcover_book_id_column.sql b/booklore-api/src/main/resources/db/migration/V74__Add_hardcover_book_id_column.sql new file mode 100644 index 00000000..18fe2a47 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V74__Add_hardcover_book_id_column.sql @@ -0,0 +1,6 @@ +-- Add numeric hardcover_book_id column to book_metadata table +-- This stores the numeric Hardcover book ID for API operations, +-- while the existing hardcover_id column stores the slug for URL linking. + +ALTER TABLE book_metadata ADD COLUMN hardcover_book_id INTEGER; + diff --git a/booklore-api/src/main/resources/db/migration/V75__Add_hardcover_book_id_locked_column.sql b/booklore-api/src/main/resources/db/migration/V75__Add_hardcover_book_id_locked_column.sql new file mode 100644 index 00000000..a3a690a0 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V75__Add_hardcover_book_id_locked_column.sql @@ -0,0 +1,3 @@ +-- Add hardcover_book_id_locked column to book_metadata table + +ALTER TABLE book_metadata ADD COLUMN hardcover_book_id_locked BOOLEAN DEFAULT FALSE; diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java b/booklore-api/src/test/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java new file mode 100644 index 00000000..6f5aa231 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/model/enums/OpdsSortOrder.java @@ -0,0 +1,13 @@ +package com.adityachandel.booklore.model.enums; + +public enum OpdsSortOrder { + RECENT, + TITLE_ASC, + TITLE_DESC, + AUTHOR_ASC, + AUTHOR_DESC, + SERIES_ASC, + SERIES_DESC, + RATING_ASC, + RATING_DESC +} diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboReadingStateServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboReadingStateServiceTest.java index 751176f7..6cfc44de 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboReadingStateServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboReadingStateServiceTest.java @@ -15,6 +15,7 @@ import com.adityachandel.booklore.repository.BookRepository; import com.adityachandel.booklore.repository.KoboReadingStateRepository; import com.adityachandel.booklore.repository.UserBookProgressRepository; import com.adityachandel.booklore.repository.UserRepository; +import com.adityachandel.booklore.service.hardcover.HardcoverSyncService; import com.adityachandel.booklore.service.kobo.KoboReadingStateBuilder; import com.adityachandel.booklore.service.kobo.KoboReadingStateService; import com.adityachandel.booklore.service.kobo.KoboSettingsService; @@ -64,6 +65,9 @@ class KoboReadingStateServiceTest { @Mock private KoboReadingStateBuilder readingStateBuilder; + @Mock + private HardcoverSyncService hardcoverSyncService; + @InjectMocks private KoboReadingStateService service; diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboStatusSyncProtectionTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboStatusSyncProtectionTest.java index 69909355..54f33cc7 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboStatusSyncProtectionTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/KoboStatusSyncProtectionTest.java @@ -14,6 +14,7 @@ import com.adityachandel.booklore.repository.BookRepository; import com.adityachandel.booklore.repository.KoboReadingStateRepository; import com.adityachandel.booklore.repository.UserBookProgressRepository; import com.adityachandel.booklore.repository.UserRepository; +import com.adityachandel.booklore.service.hardcover.HardcoverSyncService; import com.adityachandel.booklore.service.kobo.KoboReadingStateBuilder; import com.adityachandel.booklore.service.kobo.KoboReadingStateService; import com.adityachandel.booklore.service.kobo.KoboSettingsService; @@ -58,6 +59,9 @@ class KoboStatusSyncProtectionTest { @Mock private KoboReadingStateBuilder readingStateBuilder; + @Mock + private HardcoverSyncService hardcoverSyncService; + @InjectMocks private KoboReadingStateService service; diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditServiceTest.java new file mode 100644 index 00000000..f4c19626 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/BookdropBulkEditServiceTest.java @@ -0,0 +1,341 @@ +package com.adityachandel.booklore.service.bookdrop; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.BookdropBulkEditRequest; +import com.adityachandel.booklore.model.dto.response.BookdropBulkEditResult; +import com.adityachandel.booklore.model.entity.BookdropFileEntity; +import com.adityachandel.booklore.repository.BookdropFileRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class BookdropBulkEditServiceTest { + + @Mock + private BookdropFileRepository bookdropFileRepository; + + @Mock + private BookdropMetadataHelper metadataHelper; + + @InjectMocks + private BookdropBulkEditService bulkEditService; + + @Captor + private ArgumentCaptor> filesCaptor; + + private BookdropFileEntity createFileEntity(Long id, String fileName, BookMetadata metadata) { + BookdropFileEntity entity = new BookdropFileEntity(); + entity.setId(id); + entity.setFileName(fileName); + entity.setFilePath("/bookdrop/" + fileName); + return entity; + } + + @BeforeEach + void setUp() { + when(metadataHelper.getCurrentMetadata(any())).thenReturn(new BookMetadata()); + doNothing().when(metadataHelper).updateFetchedMetadata(any(), any()); + } + + @Test + void bulkEdit_WithSingleValueFields_ShouldUpdateTextAndNumericFields() { + BookMetadata existingMetadata = new BookMetadata(); + existingMetadata.setSeriesName("Old Series"); + + BookdropFileEntity file1 = createFileEntity(1L, "file1.cbz", existingMetadata); + BookdropFileEntity file2 = createFileEntity(2L, "file2.cbz", existingMetadata); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L, 2L))) + .thenReturn(List.of(1L, 2L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file1, file2)); + + BookMetadata updates = new BookMetadata(); + updates.setSeriesName("New Series"); + updates.setPublisher("Test Publisher"); + updates.setLanguage("en"); + updates.setSeriesTotal(100); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("seriesName", "publisher", "language", "seriesTotal")); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L, 2L)); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(2, result.getTotalFiles()); + assertEquals(2, result.getSuccessfullyUpdated()); + assertEquals(0, result.getFailed()); + + verify(metadataHelper, times(2)).updateFetchedMetadata(any(), any()); + verify(bookdropFileRepository, times(1)).saveAll(anyList()); + } + + @Test + void bulkEdit_WithArrayFieldsMergeMode_ShouldMergeArrays() { + BookMetadata existingMetadata = new BookMetadata(); + existingMetadata.setAuthors(new LinkedHashSet<>(List.of("Author 1"))); + existingMetadata.setCategories(new LinkedHashSet<>(List.of("Category 1"))); + + when(metadataHelper.getCurrentMetadata(any())).thenReturn(existingMetadata); + + BookdropFileEntity file = createFileEntity(1L, "file.cbz", existingMetadata); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L))) + .thenReturn(List.of(1L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file)); + + BookMetadata updates = new BookMetadata(); + updates.setAuthors(new LinkedHashSet<>(List.of("Author 2"))); + updates.setCategories(new LinkedHashSet<>(List.of("Category 2"))); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("authors", "categories")); + request.setMergeArrays(true); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L)); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(1, result.getTotalFiles()); + assertEquals(1, result.getSuccessfullyUpdated()); + assertEquals(0, result.getFailed()); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(BookMetadata.class); + verify(metadataHelper).updateFetchedMetadata(any(), metadataCaptor.capture()); + + BookMetadata captured = metadataCaptor.getValue(); + assertTrue(captured.getAuthors().contains("Author 1")); + assertTrue(captured.getAuthors().contains("Author 2")); + assertTrue(captured.getCategories().contains("Category 1")); + assertTrue(captured.getCategories().contains("Category 2")); + } + + @Test + void bulkEdit_WithArrayFieldsReplaceMode_ShouldReplaceArrays() { + BookMetadata existingMetadata = new BookMetadata(); + existingMetadata.setAuthors(new LinkedHashSet<>(List.of("Author 1"))); + + when(metadataHelper.getCurrentMetadata(any())).thenReturn(existingMetadata); + + BookdropFileEntity file = createFileEntity(1L, "file.cbz", existingMetadata); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L))) + .thenReturn(List.of(1L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file)); + + BookMetadata updates = new BookMetadata(); + updates.setAuthors(new LinkedHashSet<>(List.of("Author 2"))); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("authors")); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L)); + + bulkEditService.bulkEdit(request); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(BookMetadata.class); + verify(metadataHelper).updateFetchedMetadata(any(), metadataCaptor.capture()); + + BookMetadata captured = metadataCaptor.getValue(); + assertFalse(captured.getAuthors().contains("Author 1")); + assertTrue(captured.getAuthors().contains("Author 2")); + assertEquals(1, captured.getAuthors().size()); + } + + @Test + void bulkEdit_WithDisabledFields_ShouldNotUpdateThoseFields() { + BookMetadata existingMetadata = new BookMetadata(); + existingMetadata.setSeriesName("Original Series"); + existingMetadata.setPublisher("Original Publisher"); + + when(metadataHelper.getCurrentMetadata(any())).thenReturn(existingMetadata); + + BookdropFileEntity file = createFileEntity(1L, "file.cbz", existingMetadata); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L))) + .thenReturn(List.of(1L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file)); + + BookMetadata updates = new BookMetadata(); + updates.setSeriesName("New Series"); + updates.setPublisher("New Publisher"); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("seriesName")); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L)); + + bulkEditService.bulkEdit(request); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(BookMetadata.class); + verify(metadataHelper).updateFetchedMetadata(any(), metadataCaptor.capture()); + + BookMetadata captured = metadataCaptor.getValue(); + assertEquals("New Series", captured.getSeriesName()); + assertEquals("Original Publisher", captured.getPublisher()); + } + + @Test + void bulkEdit_WithSelectAll_ShouldProcessAllFiles() { + BookdropFileEntity file1 = createFileEntity(1L, "file1.cbz", new BookMetadata()); + BookdropFileEntity file2 = createFileEntity(2L, "file2.cbz", new BookMetadata()); + BookdropFileEntity file3 = createFileEntity(3L, "file3.cbz", new BookMetadata()); + + when(metadataHelper.resolveFileIds(true, List.of(2L), null)) + .thenReturn(List.of(1L, 3L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file1, file3)); + + BookMetadata updates = new BookMetadata(); + updates.setLanguage("en"); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("language")); + request.setMergeArrays(false); + request.setSelectAll(true); + request.setExcludedIds(List.of(2L)); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(2, result.getTotalFiles()); + assertEquals(2, result.getSuccessfullyUpdated()); + verify(metadataHelper, times(2)).updateFetchedMetadata(any(), any()); + } + + @Test + void bulkEdit_WithOneFileError_ShouldContinueWithOthers() { + BookdropFileEntity file1 = createFileEntity(1L, "file1.cbz", new BookMetadata()); + BookdropFileEntity file2 = createFileEntity(2L, "file2.cbz", new BookMetadata()); + BookdropFileEntity file3 = createFileEntity(3L, "file3.cbz", new BookMetadata()); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L, 2L, 3L))) + .thenReturn(List.of(1L, 2L, 3L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file1, file2, file3)); + + doThrow(new RuntimeException("JSON serialization error")) + .when(metadataHelper).updateFetchedMetadata(eq(file2), any()); + + BookMetadata updates = new BookMetadata(); + updates.setLanguage("en"); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("language")); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L, 2L, 3L)); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(3, result.getTotalFiles()); + assertEquals(2, result.getSuccessfullyUpdated()); + assertEquals(1, result.getFailed()); + + verify(bookdropFileRepository).saveAll(filesCaptor.capture()); + List savedFiles = filesCaptor.getValue(); + assertEquals(2, savedFiles.size()); + assertTrue(savedFiles.stream().anyMatch(f -> f.getId().equals(1L))); + assertTrue(savedFiles.stream().anyMatch(f -> f.getId().equals(3L))); + assertFalse(savedFiles.stream().anyMatch(f -> f.getId().equals(2L))); + } + + @Test + void bulkEdit_WithEmptyEnabledFields_ShouldNotUpdateAnything() { + BookdropFileEntity file = createFileEntity(1L, "file.cbz", new BookMetadata()); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L))) + .thenReturn(List.of(1L)); + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(List.of(file)); + + BookMetadata updates = new BookMetadata(); + updates.setSeriesName("New Series"); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Collections.emptySet()); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L)); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(1, result.getSuccessfullyUpdated()); + + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(BookMetadata.class); + verify(metadataHelper).updateFetchedMetadata(any(), metadataCaptor.capture()); + + assertNull(metadataCaptor.getValue().getSeriesName()); + } + + @Test + void bulkEdit_WithLargeSelection_ShouldProcessInBatches() { + List batch1 = new ArrayList<>(); + List batch2 = new ArrayList<>(); + List batch3 = new ArrayList<>(); + List manyIds = new ArrayList<>(); + + for (long i = 1; i <= 1500; i++) { + manyIds.add(i); + BookdropFileEntity file = createFileEntity(i, "file" + i + ".cbz", new BookMetadata()); + if (i <= 500) { + batch1.add(file); + } else if (i <= 1000) { + batch2.add(file); + } else { + batch3.add(file); + } + } + + when(metadataHelper.resolveFileIds(false, null, manyIds)) + .thenReturn(manyIds); + + when(bookdropFileRepository.findAllById(anyList())) + .thenReturn(batch1, batch2, batch3); + + BookMetadata updates = new BookMetadata(); + updates.setLanguage("en"); + + BookdropBulkEditRequest request = new BookdropBulkEditRequest(); + request.setFields(updates); + request.setEnabledFields(Set.of("language")); + request.setMergeArrays(false); + request.setSelectAll(false); + request.setSelectedIds(manyIds); + + BookdropBulkEditResult result = bulkEditService.bulkEdit(request); + + assertEquals(1500, result.getTotalFiles()); + assertEquals(1500, result.getSuccessfullyUpdated()); + assertEquals(0, result.getFailed()); + + verify(bookdropFileRepository, times(3)).findAllById(anyList()); + verify(bookdropFileRepository, times(3)).saveAll(anyList()); + } +} diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractorTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractorTest.java new file mode 100644 index 00000000..1ed67485 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/bookdrop/FilenamePatternExtractorTest.java @@ -0,0 +1,644 @@ +package com.adityachandel.booklore.service.bookdrop; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import com.adityachandel.booklore.model.dto.request.BookdropPatternExtractRequest; +import com.adityachandel.booklore.model.dto.response.BookdropPatternExtractResult; +import com.adityachandel.booklore.model.entity.BookdropFileEntity; +import com.adityachandel.booklore.repository.BookdropFileRepository; +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 java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FilenamePatternExtractorTest { + + @Mock + private BookdropFileRepository bookdropFileRepository; + + @Mock + private BookdropMetadataHelper metadataHelper; + + @InjectMocks + private FilenamePatternExtractor extractor; + + private BookdropFileEntity createFileEntity(Long id, String fileName) { + BookdropFileEntity entity = new BookdropFileEntity(); + entity.setId(id); + entity.setFileName(fileName); + entity.setFilePath("/bookdrop/" + fileName); + return entity; + } + + @Test + void extractFromFilename_WithSeriesAndChapter_ShouldExtractBoth() { + String filename = "Chronicles of Earth - Ch 25.cbz"; + String pattern = "{SeriesName} - Ch {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(25.0f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WithVolumeAndIssuePattern_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth Vol.3 (of 150).cbz"; + String pattern = "{SeriesName} Vol.{SeriesNumber} (of {SeriesTotal})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(3.0f, result.getSeriesNumber()); + assertEquals(150, result.getSeriesTotal()); + } + + @Test + void extractFromFilename_WithPublishedYearPattern_ShouldExtractYear() { + String filename = "Chronicles of Earth (2016) 001.cbz"; + String pattern = "{SeriesName} ({Published:yyyy}) {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(2016, result.getPublishedDate().getYear()); + assertEquals(1.0f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WithAuthorAndTitle_ShouldExtractBoth() { + String filename = "John Smith - The Lost City.epub"; + String pattern = "{Authors} - {Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals(Set.of("John Smith"), result.getAuthors()); + assertEquals("The Lost City", result.getTitle()); + } + + @Test + void extractFromFilename_WithMultipleAuthors_ShouldParseAll() { + String filename = "John Smith, Jane Doe - The Lost City.epub"; + String pattern = "{Authors} - {Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertTrue(result.getAuthors().contains("John Smith")); + assertTrue(result.getAuthors().contains("Jane Doe")); + assertEquals("The Lost City", result.getTitle()); + } + + @Test + void extractFromFilename_WithDecimalSeriesNumber_ShouldParseCorrectly() { + String filename = "Chronicles of Earth - Ch 10.5.cbz"; + String pattern = "{SeriesName} - Ch {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(10.5f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WithNonMatchingPattern_ShouldReturnNull() { + String filename = "Random File Name.pdf"; + String pattern = "{SeriesName} - Ch {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNull(result); + } + + @Test + void extractFromFilename_WithNullPattern_ShouldReturnNull() { + String filename = "Test File.pdf"; + + BookMetadata result = extractor.extractFromFilename(filename, null); + + assertNull(result); + } + + @Test + void extractFromFilename_WithEmptyPattern_ShouldReturnNull() { + String filename = "Test File.pdf"; + + BookMetadata result = extractor.extractFromFilename(filename, ""); + + assertNull(result); + } + + @Test + void extractFromFilename_WithPublisherYearAndIssue_ShouldExtractAll() { + String filename = "Epic Press - Chronicles of Earth #001 (2011).cbz"; + String pattern = "{Publisher} - {SeriesName} #{SeriesNumber} ({Published:yyyy})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Epic Press", result.getPublisher()); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(1.0f, result.getSeriesNumber()); + assertEquals(2011, result.getPublishedDate().getYear()); + } + + @Test + void extractFromFilename_WithLanguageTag_ShouldExtractLanguage() { + String filename = "Chronicles of Earth - Ch 500 [EN].cbz"; + String pattern = "{SeriesName} - Ch {SeriesNumber} [{Language}]"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(500.0f, result.getSeriesNumber()); + assertEquals("EN", result.getLanguage()); + } + + @Test + void bulkExtract_WithPreviewMode_ShouldReturnExtractionResults() { + BookdropFileEntity file1 = createFileEntity(1L, "Chronicles A - Ch 1.cbz"); + BookdropFileEntity file2 = createFileEntity(2L, "Chronicles B - Ch 2.cbz"); + BookdropFileEntity file3 = createFileEntity(3L, "Random Name.cbz"); + + BookdropPatternExtractRequest request = new BookdropPatternExtractRequest(); + request.setPattern("{SeriesName} - Ch {SeriesNumber}"); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L, 2L, 3L)); + request.setPreview(true); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L, 2L, 3L))) + .thenReturn(List.of(1L, 2L, 3L)); + when(bookdropFileRepository.findAllById(anyList())).thenReturn(List.of(file1, file2, file3)); + + BookdropPatternExtractResult result = extractor.bulkExtract(request); + + assertNotNull(result); + assertEquals(3, result.getTotalFiles()); + assertEquals(2, result.getSuccessfullyExtracted()); + assertEquals(1, result.getFailed()); + + var successResults = result.getResults().stream() + .filter(BookdropPatternExtractResult.FileExtractionResult::isSuccess) + .toList(); + assertEquals(2, successResults.size()); + } + + @Test + void bulkExtract_WithFullExtraction_ShouldProcessAndPersistAll() { + BookdropFileEntity file1 = createFileEntity(1L, "Chronicles A - Ch 1.cbz"); + BookdropFileEntity file2 = createFileEntity(2L, "Chronicles B - Ch 2.cbz"); + BookdropFileEntity file3 = createFileEntity(3L, "Random Name.cbz"); + + BookdropPatternExtractRequest request = new BookdropPatternExtractRequest(); + request.setPattern("{SeriesName} - Ch {SeriesNumber}"); + request.setSelectAll(false); + request.setSelectedIds(List.of(1L, 2L, 3L)); + request.setPreview(false); + + when(metadataHelper.resolveFileIds(false, null, List.of(1L, 2L, 3L))) + .thenReturn(List.of(1L, 2L, 3L)); + when(bookdropFileRepository.findAllById(anyList())).thenReturn(List.of(file1, file2, file3)); + when(metadataHelper.getCurrentMetadata(any())).thenReturn(new BookMetadata()); + + BookdropPatternExtractResult result = extractor.bulkExtract(request); + + assertNotNull(result); + assertEquals(3, result.getTotalFiles()); + assertEquals(2, result.getSuccessfullyExtracted()); + assertEquals(1, result.getFailed()); + + // Verify metadata was updated for successful extractions (2 files matched pattern) + verify(metadataHelper, times(2)).updateFetchedMetadata(any(), any()); + // Verify all files were saved (even the one that failed extraction keeps original metadata) + verify(bookdropFileRepository, times(1)).saveAll(anyList()); + } + + @Test + void extractFromFilename_WithSpecialCharacters_ShouldHandleCorrectly() { + String filename = "Chronicles (Special Edition) - Ch 5.cbz"; + String pattern = "{SeriesName} - Ch {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles (Special Edition)", result.getSeriesName()); + assertEquals(5.0f, result.getSeriesNumber()); + } + + // ===== Greedy Matching Tests ===== + + @Test + void extractFromFilename_SeriesNameOnly_ShouldCaptureFullName() { + String filename = "Chronicles of Earth.cbz"; + String pattern = "{SeriesName}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + } + + @Test + void extractFromFilename_TitleOnly_ShouldCaptureFullTitle() { + String filename = "The Last Kingdom.epub"; + String pattern = "{Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Last Kingdom", result.getTitle()); + } + + // ===== Complex Pattern Tests ===== + + @Test + void extractFromFilename_SeriesNumberAndTitle_ShouldExtractBoth() { + String filename = "Chronicles of Earth 01 - The Beginning.epub"; + String pattern = "{SeriesName} {SeriesNumber} - {Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(1.0f, result.getSeriesNumber()); + assertEquals("The Beginning", result.getTitle()); + } + + @Test + void extractFromFilename_AuthorSeriesTitleFormat_ShouldExtractAll() { + String filename = "Chronicles of Earth 07 - The Final Battle - John Smith.epub"; + String pattern = "{SeriesName} {SeriesNumber} - {Title} - {Authors}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(7.0f, result.getSeriesNumber()); + assertEquals("The Final Battle", result.getTitle()); + assertEquals(Set.of("John Smith"), result.getAuthors()); + } + + @Test + void extractFromFilename_AuthorTitleYear_ShouldExtractAll() { + String filename = "John Smith - The Lost City (1949).epub"; + String pattern = "{Authors} - {Title} ({Published:yyyy})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals(Set.of("John Smith"), result.getAuthors()); + assertEquals("The Lost City", result.getTitle()); + assertEquals(1949, result.getPublishedDate().getYear()); + } + + @Test + void extractFromFilename_AuthorWithCommas_ShouldParseProperly() { + String filename = "Smith, John R. - The Lost City.epub"; + String pattern = "{Authors} - {Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals(Set.of("Smith", "John R."), result.getAuthors()); + assertEquals("The Lost City", result.getTitle()); + } + + @Test + void extractFromFilename_PartNumberFormat_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth - Part 2 - Rising Darkness.epub"; + String pattern = "{SeriesName} - Part {SeriesNumber} - {Title}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(2.0f, result.getSeriesNumber()); + assertEquals("Rising Darkness", result.getTitle()); + } + + @Test + void extractFromFilename_PublisherBracketFormat_ShouldExtractCorrectly() { + String filename = "[Epic Press] Chronicles of Earth Vol.5 [5 of 20].epub"; + String pattern = "[{Publisher}] {SeriesName} Vol.{SeriesNumber} [* of {SeriesTotal}]"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Epic Press", result.getPublisher()); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(5.0f, result.getSeriesNumber()); + assertEquals(20, result.getSeriesTotal()); + } + + @Test + void extractFromFilename_CalibreStyleFormat_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth 01 The Beginning - John Smith.epub"; + String pattern = "{SeriesName} {SeriesNumber} {Title} - {Authors}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(1.0f, result.getSeriesNumber()); + assertEquals("The Beginning", result.getTitle()); + assertEquals(Set.of("John Smith"), result.getAuthors()); + } + + // ===== New Placeholder Tests ===== + + @Test + void extractFromFilename_WithSubtitle_ShouldExtractBoth() { + String filename = "The Lost City - A Tale of Adventure.epub"; + String pattern = "{Title} - {Subtitle}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals("A Tale of Adventure", result.getSubtitle()); + } + + @Test + void extractFromFilename_WithISBN13_ShouldExtractISBN13() { + String filename = "The Lost City [1234567890123].epub"; + String pattern = "{Title} [{ISBN13}]"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals("1234567890123", result.getIsbn13()); + } + + @Test + void extractFromFilename_WithISBN10_ShouldExtractCorrectly() { + String filename = "Chronicles of Tomorrow - 0553293354.epub"; + String pattern = "{Title} - {ISBN10}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Tomorrow", result.getTitle()); + assertEquals("0553293354", result.getIsbn10()); + } + + @Test + void extractFromFilename_WithISBN10EndingInX_ShouldExtractCorrectly() { + String filename = "Test Book - 043942089X.epub"; + String pattern = "{Title} - {ISBN10}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Test Book", result.getTitle()); + assertEquals("043942089X", result.getIsbn10()); + } + + @Test + void extractFromFilename_WithASIN_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth - B001234567.epub"; + String pattern = "{Title} - {ASIN}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getTitle()); + assertEquals("B001234567", result.getAsin()); + } + + // ===== Published Date Format Tests ===== + + @Test + void extractFromFilename_WithPublishedDateYYYYMMDD_ShouldExtractCorrectly() { + String filename = "The Lost City - 1925-04-10.epub"; + String pattern = "{Title} - {Published:yyyy-MM-dd}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals(1925, result.getPublishedDate().getYear()); + assertEquals(4, result.getPublishedDate().getMonthValue()); + assertEquals(10, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_WithPublishedDateCompact_ShouldExtractCorrectly() { + String filename = "Chronicles of Tomorrow_19650801.epub"; + String pattern = "{Title}_{Published:yyyyMMdd}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Tomorrow", result.getTitle()); + assertEquals(1965, result.getPublishedDate().getYear()); + assertEquals(8, result.getPublishedDate().getMonthValue()); + assertEquals(1, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_WithPublishedDateDots_ShouldExtractCorrectly() { + String filename = "Chronicles of Tomorrow (1951.05.01).epub"; + String pattern = "{Title} ({Published:yyyy.MM.dd})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Tomorrow", result.getTitle()); + assertEquals(1951, result.getPublishedDate().getYear()); + assertEquals(5, result.getPublishedDate().getMonthValue()); + assertEquals(1, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_WithPublishedDateDashes_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth [05-15-2020].epub"; + String pattern = "{Title} [{Published:MM-dd-yyyy}]"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getTitle()); + assertEquals(2020, result.getPublishedDate().getYear()); + assertEquals(5, result.getPublishedDate().getMonthValue()); + assertEquals(15, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_WithPublishedDateSingleDigits_ShouldExtractCorrectly() { + String filename = "Chronicles of Earth - 2023-1-5.epub"; + String pattern = "{Title} - {Published:yyyy-M-d}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getTitle()); + assertEquals(2023, result.getPublishedDate().getYear()); + assertEquals(1, result.getPublishedDate().getMonthValue()); + assertEquals(5, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_ComplexPatternWithMultiplePlaceholders_ShouldExtractAll() { + String filename = "Chronicles of Earth - The Beginning [1234567890123] - 2020-05-15.epub"; + String pattern = "{SeriesName} - {Title} [{ISBN13}] - {Published:yyyy-MM-dd}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals("The Beginning", result.getTitle()); + assertEquals("1234567890123", result.getIsbn13()); + assertEquals(2020, result.getPublishedDate().getYear()); + assertEquals(5, result.getPublishedDate().getMonthValue()); + assertEquals(15, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_PublishedWithoutFormat_AutoDetectsISODate() { + String filename = "The Lost City (2023-05-15).epub"; + String pattern = "{Title} ({Published})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals(2023, result.getPublishedDate().getYear()); + assertEquals(5, result.getPublishedDate().getMonthValue()); + assertEquals(15, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_PublishedWithoutFormat_AutoDetectsCompactDate() { + String filename = "The Beginning [20231225].epub"; + String pattern = "{Title} [{Published}]"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Beginning", result.getTitle()); + assertEquals(2023, result.getPublishedDate().getYear()); + assertEquals(12, result.getPublishedDate().getMonthValue()); + assertEquals(25, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_PublishedWithoutFormat_AutoDetectsYear() { + String filename = "The Lost City (2023).epub"; + String pattern = "{Title} ({Published})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals(2023, result.getPublishedDate().getYear()); + assertEquals(1, result.getPublishedDate().getMonthValue()); + assertEquals(1, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_PublishedWithoutFormat_AutoDetectsTwoDigitYear() { + String filename = "Chronicles of Tomorrow (99).epub"; + String pattern = "{Title} ({Published})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Tomorrow", result.getTitle()); + assertEquals(1999, result.getPublishedDate().getYear()); + } + + @Test + void extractFromFilename_PublishedWithoutFormat_AutoDetectsFlexibleFormat() { + String filename = "Tomorrow (15|05|2023).epub"; + String pattern = "{Title} ({Published})"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Tomorrow", result.getTitle()); + assertEquals(2023, result.getPublishedDate().getYear()); + assertEquals(5, result.getPublishedDate().getMonthValue()); + assertEquals(15, result.getPublishedDate().getDayOfMonth()); + } + + @Test + void extractFromFilename_WildcardBeforePlaceholder_SkipsUnwantedText() { + String filename = "[Extra] Chronicles of Earth - Ch 42.cbz"; + String pattern = "[*] {SeriesName} - Ch {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(42.0f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WildcardBetweenPlaceholders_SkipsMiddleText() { + String filename = "The Lost City (extra) John Smith.epub"; + String pattern = "{Title} (*) {Authors}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("The Lost City", result.getTitle()); + assertEquals(Set.of("John Smith"), result.getAuthors()); + } + + @Test + void extractFromFilename_WildcardAtEnd_SkipsTrailingText() { + String filename = "Chronicles of Earth v1 - extra.cbz"; + String pattern = "{SeriesName} v{SeriesNumber} - *"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(1.0f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WildcardAtEnd_AllowsPartialMatch() { + String filename = "Chronicles of Earth - Chapter 20.cbz"; + String pattern = "{SeriesName} - * {SeriesNumber}"; + + BookMetadata result = extractor.extractFromFilename(filename, pattern); + + assertNotNull(result); + assertEquals("Chronicles of Earth", result.getSeriesName()); + assertEquals(20.0f, result.getSeriesNumber()); + } + + @Test + void extractFromFilename_WildcardWithVariousPlacements_HandlesCorrectly() { + String filename1 = "Chronicles of Tomorrow - Chapter 8.1 (2025).cbz"; + String pattern1 = "{SeriesName} - * {SeriesNumber}"; + BookMetadata result1 = extractor.extractFromFilename(filename1, pattern1); + assertNotNull(result1); + assertEquals("Chronicles of Tomorrow", result1.getSeriesName()); + assertEquals(8.1f, result1.getSeriesNumber()); + + String filename2 = "Junk - Chapter 20.cbz"; + String pattern2 = "* - Chapter {SeriesNumber}"; + BookMetadata result2 = extractor.extractFromFilename(filename2, pattern2); + assertNotNull(result2); + assertEquals(20.0f, result2.getSeriesNumber()); + } +} + diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncServiceTest.java new file mode 100644 index 00000000..e168ddee --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/hardcover/HardcoverSyncServiceTest.java @@ -0,0 +1,439 @@ +package com.adityachandel.booklore.service.hardcover; + +import com.adityachandel.booklore.model.dto.settings.AppSettings; +import com.adityachandel.booklore.model.dto.settings.MetadataProviderSettings; +import com.adityachandel.booklore.model.entity.BookEntity; +import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.repository.BookRepository; +import com.adityachandel.booklore.service.appsettings.AppSettingService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.web.client.RestClient; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +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 HardcoverSyncServiceTest { + + @Mock + private AppSettingService appSettingService; + + @Mock + private BookRepository bookRepository; + + @Mock + private RestClient restClient; + + @Mock + private RestClient.RequestBodyUriSpec requestBodyUriSpec; + + @Mock + private RestClient.RequestBodySpec requestBodySpec; + + @Mock + private RestClient.ResponseSpec responseSpec; + + private HardcoverSyncService service; + + private BookEntity testBook; + private BookMetadataEntity testMetadata; + private AppSettings appSettings; + private MetadataProviderSettings.Hardcover hardcoverSettings; + + private static final Long TEST_BOOK_ID = 100L; + + @BeforeEach + void setUp() throws Exception { + // Create service with mocked dependencies + service = new HardcoverSyncService(appSettingService, bookRepository); + + // Inject our mocked restClient using reflection + Field restClientField = HardcoverSyncService.class.getDeclaredField("restClient"); + restClientField.setAccessible(true); + restClientField.set(service, restClient); + + testBook = new BookEntity(); + testBook.setId(TEST_BOOK_ID); + + testMetadata = new BookMetadataEntity(); + testMetadata.setIsbn13("9781234567890"); + testMetadata.setPageCount(300); + testBook.setMetadata(testMetadata); + + appSettings = new AppSettings(); + MetadataProviderSettings metadataSettings = new MetadataProviderSettings(); + hardcoverSettings = new MetadataProviderSettings.Hardcover(); + hardcoverSettings.setEnabled(true); + hardcoverSettings.setApiKey("test-api-key"); + metadataSettings.setHardcover(hardcoverSettings); + appSettings.setMetadataProviderSettings(metadataSettings); + + when(appSettingService.getAppSettings()).thenReturn(appSettings); + when(bookRepository.findById(TEST_BOOK_ID)).thenReturn(Optional.of(testBook)); + + // Setup RestClient mock chain - handles multiple calls + when(restClient.post()).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.uri(anyString())).thenReturn(requestBodySpec); + when(requestBodySpec.header(anyString(), anyString())).thenReturn(requestBodySpec); + when(requestBodySpec.body(any())).thenReturn(requestBodySpec); + when(requestBodySpec.retrieve()).thenReturn(responseSpec); + } + + // === Tests for skipping sync (no API calls should be made) === + + @Test + @DisplayName("Should skip sync when Hardcover is not enabled") + void syncProgressToHardcover_whenHardcoverDisabled_shouldSkip() { + hardcoverSettings.setEnabled(false); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when API key is missing") + void syncProgressToHardcover_whenApiKeyMissing_shouldSkip() { + hardcoverSettings.setApiKey(null); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when API key is blank") + void syncProgressToHardcover_whenApiKeyBlank_shouldSkip() { + hardcoverSettings.setApiKey(" "); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when progress is null") + void syncProgressToHardcover_whenProgressNull_shouldSkip() { + service.syncProgressToHardcover(TEST_BOOK_ID, null); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when book not found") + void syncProgressToHardcover_whenBookNotFound_shouldSkip() { + when(bookRepository.findById(TEST_BOOK_ID)).thenReturn(Optional.empty()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when book has no metadata") + void syncProgressToHardcover_whenNoMetadata_shouldSkip() { + testBook.setMetadata(null); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + @Test + @DisplayName("Should skip sync when no ISBN available") + void syncProgressToHardcover_whenNoIsbn_shouldSkip() { + testMetadata.setIsbn13(null); + testMetadata.setIsbn10(null); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, never()).post(); + } + + // === Tests for successful sync (API calls should be made) === + + @Test + @DisplayName("Should use stored hardcoverBookId when available") + void syncProgressToHardcover_withStoredBookId_shouldUseStoredId() { + testMetadata.setHardcoverBookId(12345); + testMetadata.setPageCount(300); + + // Mock successful responses for the chain + when(responseSpec.body(Map.class)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + // Verify API was called at least once (using stored ID, no search needed) + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should search by ISBN when hardcoverBookId is not stored") + void syncProgressToHardcover_withoutStoredBookId_shouldSearchByIsbn() { + // Mock successful responses for the chain + when(responseSpec.body(Map.class)) + .thenReturn(createSearchResponse(12345, 300)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + // Verify API was called at least once + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should skip further processing when book not found on Hardcover") + void syncProgressToHardcover_whenBookNotFoundOnHardcover_shouldSkipAfterSearch() { + // Mock: search returns empty results + when(responseSpec.body(Map.class)).thenReturn(createEmptySearchResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + // Should call search only + verify(restClient, times(1)).post(); + } + + @Test + @DisplayName("Should set status to READ when progress >= 99%") + void syncProgressToHardcover_whenProgress99Percent_shouldMakeApiCalls() { + testMetadata.setHardcoverBookId(12345); + testMetadata.setPageCount(300); + + when(responseSpec.body(Map.class)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 99.0f); + + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should set status to CURRENTLY_READING when progress < 99%") + void syncProgressToHardcover_whenProgressLessThan99_shouldMakeApiCalls() { + testMetadata.setHardcoverBookId(12345); + testMetadata.setPageCount(300); + + when(responseSpec.body(Map.class)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should handle existing user_book gracefully") + void syncProgressToHardcover_whenUserBookExists_shouldFindExisting() { + testMetadata.setHardcoverBookId(12345); + + // Mock: insert_user_book returns error, then find existing, then create progress + when(responseSpec.body(Map.class)) + .thenReturn(createInsertUserBookResponse(null, "Book already exists")) + .thenReturn(createFindUserBookResponse(5001)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should update existing reading progress") + void syncProgressToHardcover_whenProgressExists_shouldUpdate() { + testMetadata.setHardcoverBookId(12345); + + // Mock: insert_user_book -> find existing read -> update read + when(responseSpec.body(Map.class)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createFindUserBookReadResponse(6001)) + .thenReturn(createUpdateUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, atLeastOnce()).post(); + } + + @Test + @DisplayName("Should use ISBN10 when ISBN13 is missing") + void syncProgressToHardcover_whenIsbn13Missing_shouldUseIsbn10() { + testMetadata.setIsbn13(null); + testMetadata.setIsbn10("1234567890"); + + when(responseSpec.body(Map.class)) + .thenReturn(createSearchResponse(12345, 300)) + .thenReturn(createInsertUserBookResponse(5001, null)) + .thenReturn(createEmptyUserBookReadsResponse()) + .thenReturn(createInsertUserBookReadResponse()); + + service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f); + + verify(restClient, atLeastOnce()).post(); + } + + // === Tests for error handling === + + @Test + @DisplayName("Should handle API errors gracefully") + void syncProgressToHardcover_whenApiError_shouldNotThrow() { + testMetadata.setHardcoverBookId(12345); + + when(responseSpec.body(Map.class)).thenReturn(Map.of("errors", List.of(Map.of("message", "Unauthorized")))); + + assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f)); + } + + @Test + @DisplayName("Should handle null response gracefully") + void syncProgressToHardcover_whenResponseNull_shouldNotThrow() { + testMetadata.setHardcoverBookId(12345); + + when(responseSpec.body(Map.class)).thenReturn(null); + + assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f)); + } + + // === Helper methods to create mock responses === + + private Map createSearchResponse(Integer bookId, Integer pages) { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map search = new HashMap<>(); + Map results = new HashMap<>(); + Map hit = new HashMap<>(); + Map document = new HashMap<>(); + + document.put("id", bookId.toString()); + document.put("pages", pages); + hit.put("document", document); + results.put("hits", List.of(hit)); + search.put("results", results); + data.put("search", search); + response.put("data", data); + + return response; + } + + private Map createEmptySearchResponse() { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map search = new HashMap<>(); + Map results = new HashMap<>(); + + results.put("hits", List.of()); + search.put("results", results); + data.put("search", search); + response.put("data", data); + + return response; + } + + private Map createInsertUserBookResponse(Integer userBookId, String error) { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map insertResult = new HashMap<>(); + + if (userBookId != null) { + Map userBook = new HashMap<>(); + userBook.put("id", userBookId); + insertResult.put("user_book", userBook); + } + if (error != null) { + insertResult.put("error", error); + } + + data.put("insert_user_book", insertResult); + response.put("data", data); + + return response; + } + + private Map createFindUserBookResponse(Integer userBookId) { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map me = new HashMap<>(); + Map userBook = new HashMap<>(); + + userBook.put("id", userBookId); + me.put("user_books", List.of(userBook)); + data.put("me", me); + response.put("data", data); + + return response; + } + + private Map createInsertUserBookReadResponse() { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map insertResult = new HashMap<>(); + Map userBookRead = new HashMap<>(); + + userBookRead.put("id", 6001); + insertResult.put("user_book_read", userBookRead); + data.put("insert_user_book_read", insertResult); + response.put("data", data); + + return response; + } + + private Map createFindUserBookReadResponse(Integer readId) { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map read = new HashMap<>(); + + read.put("id", readId); + data.put("user_book_reads", List.of(read)); + response.put("data", data); + + return response; + } + + private Map createEmptyUserBookReadsResponse() { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + + data.put("user_book_reads", List.of()); + response.put("data", data); + + return response; + } + + private Map createUpdateUserBookReadResponse() { + Map response = new HashMap<>(); + Map data = new HashMap<>(); + Map updateResult = new HashMap<>(); + Map userBookRead = new HashMap<>(); + + userBookRead.put("id", 6001); + userBookRead.put("progress", 50); + updateResult.put("user_book_read", userBookRead); + data.put("update_user_book_read", updateResult); + response.put("data", data); + + return response; + } +} diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java index 26c5fbd1..6910f99c 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java @@ -42,7 +42,7 @@ class CbxConversionIntegrationTest { File testCbzFile = createTestComicCbzFile(); BookEntity bookMetadata = createTestBookMetadata(); - File epubFile = conversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), bookMetadata); + File epubFile = conversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), bookMetadata,85); assertThat(epubFile) .exists() diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java index dfbad12a..ed4d4170 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java @@ -42,7 +42,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithValidCbzFile_ShouldGenerateValidEpub() throws IOException, TemplateException, RarException { - File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), testBookEntity); + File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), testBookEntity,85); assertThat(epubFile).exists(); assertThat(epubFile.getName()).endsWith(".epub"); @@ -53,7 +53,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithNullCbxFile_ShouldThrowException() { - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(null, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(null, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid CBX file"); } @@ -62,7 +62,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithNonExistentFile_ShouldThrowException() { File nonExistentFile = new File(tempDir.toFile(), "non-existent.cbz"); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(nonExistentFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(nonExistentFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid CBX file"); } @@ -71,14 +71,14 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithUnsupportedFileFormat_ShouldThrowException() throws IOException { File unsupportedFile = Files.createFile(tempDir.resolve("test.txt")).toFile(); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(unsupportedFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(unsupportedFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unsupported file format"); } @Test void convertCbxToEpub_WithNullTempDir_ShouldThrowException() { - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(testCbzFile, null, testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(testCbzFile, null, testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid temp directory"); } @@ -87,7 +87,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithEmptyCbzFile_ShouldThrowException() throws IOException { File emptyCbzFile = createEmptyCbzFile(); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(emptyCbzFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(emptyCbzFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("No valid images found"); } @@ -118,7 +118,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithNullBookEntity_ShouldUseDefaultMetadata() throws IOException, TemplateException, RarException { - File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), null); + File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), null,85); assertThat(epubFile).exists(); verifyEpubStructure(epubFile); @@ -128,7 +128,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithMultipleImages_ShouldPreservePageOrder() throws IOException, TemplateException, RarException { File multiPageCbzFile = createMultiPageCbzFile(); - File epubFile = cbxConversionService.convertCbxToEpub(multiPageCbzFile, tempDir.toFile(), testBookEntity); + File epubFile = cbxConversionService.convertCbxToEpub(multiPageCbzFile, tempDir.toFile(), testBookEntity,85); assertThat(epubFile).exists(); verifyPageOrderInEpub(epubFile, 5); diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractorTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractorTest.java new file mode 100644 index 00000000..fe032988 --- /dev/null +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/extractor/Fb2MetadataExtractorTest.java @@ -0,0 +1,885 @@ +package com.adityachandel.booklore.service.metadata.extractor; + +import com.adityachandel.booklore.model.dto.BookMetadata; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; + +class Fb2MetadataExtractorTest { + + private static final String DEFAULT_TITLE = "The Seven Poor Travellers"; + private static final String DEFAULT_AUTHOR_FIRST = "Charles"; + private static final String DEFAULT_AUTHOR_LAST = "Dickens"; + private static final String DEFAULT_AUTHOR_FULL = "Charles Dickens"; + private static final String DEFAULT_GENRE = "antique"; + private static final String DEFAULT_LANGUAGE = "ru"; + private static final String DEFAULT_PUBLISHER = "Test Publisher"; + private static final String DEFAULT_ISBN = "9781234567890"; + private static final String DEFAULT_SERIES = "Great Works"; + + private Fb2MetadataExtractor extractor; + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + extractor = new Fb2MetadataExtractor(); + } + + @Nested + @DisplayName("Basic Metadata Extraction Tests") + class BasicMetadataTests { + + @Test + @DisplayName("Should extract title from title-info") + void extractMetadata_withTitle_returnsTitle() throws IOException { + String fb2Content = createFb2WithTitleInfo( + DEFAULT_TITLE, + DEFAULT_AUTHOR_FIRST, + DEFAULT_AUTHOR_LAST, + DEFAULT_GENRE, + DEFAULT_LANGUAGE + ); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(DEFAULT_TITLE, result.getTitle()); + } + + @Test + @DisplayName("Should extract author name from title-info") + void extractMetadata_withAuthor_returnsAuthor() throws IOException { + String fb2Content = createFb2WithTitleInfo( + DEFAULT_TITLE, + DEFAULT_AUTHOR_FIRST, + DEFAULT_AUTHOR_LAST, + DEFAULT_GENRE, + DEFAULT_LANGUAGE + ); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getAuthors()); + assertEquals(1, result.getAuthors().size()); + assertTrue(result.getAuthors().contains(DEFAULT_AUTHOR_FULL)); + } + + @Test + @DisplayName("Should extract multiple authors") + void extractMetadata_withMultipleAuthors_returnsAllAuthors() throws IOException { + String fb2Content = createFb2WithMultipleAuthors(); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getAuthors()); + assertEquals(2, result.getAuthors().size()); + assertTrue(result.getAuthors().contains("Charles Dickens")); + assertTrue(result.getAuthors().contains("Jane Austen")); + } + + @Test + @DisplayName("Should extract genre as category") + void extractMetadata_withGenre_returnsCategory() throws IOException { + String fb2Content = createFb2WithTitleInfo( + DEFAULT_TITLE, + DEFAULT_AUTHOR_FIRST, + DEFAULT_AUTHOR_LAST, + DEFAULT_GENRE, + DEFAULT_LANGUAGE + ); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getCategories()); + assertTrue(result.getCategories().contains(DEFAULT_GENRE)); + } + + @Test + @DisplayName("Should extract multiple genres as categories") + void extractMetadata_withMultipleGenres_returnsAllCategories() throws IOException { + String fb2Content = createFb2WithMultipleGenres(); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getCategories()); + assertTrue(result.getCategories().contains("fiction")); + assertTrue(result.getCategories().contains("drama")); + } + + @Test + @DisplayName("Should extract language") + void extractMetadata_withLanguage_returnsLanguage() throws IOException { + String fb2Content = createFb2WithTitleInfo( + DEFAULT_TITLE, + DEFAULT_AUTHOR_FIRST, + DEFAULT_AUTHOR_LAST, + DEFAULT_GENRE, + DEFAULT_LANGUAGE + ); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(DEFAULT_LANGUAGE, result.getLanguage()); + } + + @Test + @DisplayName("Should extract annotation as description") + void extractMetadata_withAnnotation_returnsDescription() throws IOException { + String annotation = "This is a test book description"; + String fb2Content = createFb2WithAnnotation(annotation); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getDescription()); + assertTrue(result.getDescription().contains(annotation)); + } + } + + @Nested + @DisplayName("Date Extraction Tests") + class DateExtractionTests { + + @Test + @DisplayName("Should extract date from title-info") + void extractMetadata_withDate_returnsDate() throws IOException { + String fb2Content = createFb2WithDate("2024-06-15"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(LocalDate.of(2024, 6, 15), result.getPublishedDate()); + } + + @Test + @DisplayName("Should extract year-only date") + void extractMetadata_withYearOnly_returnsDateWithJanuary1st() throws IOException { + String fb2Content = createFb2WithDate("2024"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(LocalDate.of(2024, 1, 1), result.getPublishedDate()); + } + + @Test + @DisplayName("Should handle date with value attribute") + void extractMetadata_withDateValue_returnsDate() throws IOException { + String fb2Content = createFb2WithDateValue("2024-06-15", "June 15, 2024"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(LocalDate.of(2024, 6, 15), result.getPublishedDate()); + } + } + + @Nested + @DisplayName("Series Metadata Tests") + class SeriesMetadataTests { + + @Test + @DisplayName("Should extract series name from sequence") + void extractMetadata_withSequence_returnsSeriesName() throws IOException { + String fb2Content = createFb2WithSequence(DEFAULT_SERIES, "3"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(DEFAULT_SERIES, result.getSeriesName()); + } + + @Test + @DisplayName("Should extract series number from sequence") + void extractMetadata_withSequence_returnsSeriesNumber() throws IOException { + String fb2Content = createFb2WithSequence(DEFAULT_SERIES, "3"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(3.0f, result.getSeriesNumber(), 0.001); + } + + @Test + @DisplayName("Should handle decimal series numbers") + void extractMetadata_withDecimalSequence_returnsDecimalSeriesNumber() throws IOException { + String fb2Content = createFb2WithSequence(DEFAULT_SERIES, "2.5"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(2.5f, result.getSeriesNumber(), 0.001); + } + } + + @Nested + @DisplayName("Publisher Info Extraction Tests") + class PublisherInfoTests { + + @Test + @DisplayName("Should extract publisher from publish-info") + void extractMetadata_withPublisher_returnsPublisher() throws IOException { + String fb2Content = createFb2WithPublishInfo(DEFAULT_PUBLISHER, "2024", DEFAULT_ISBN); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(DEFAULT_PUBLISHER, result.getPublisher()); + } + + @Test + @DisplayName("Should extract year from publish-info") + void extractMetadata_withPublishYear_returnsDate() throws IOException { + String fb2Content = createFb2WithPublishInfo(DEFAULT_PUBLISHER, "2024", null); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals(LocalDate.of(2024, 1, 1), result.getPublishedDate()); + } + + @Test + @DisplayName("Should extract ISBN-13 from publish-info") + void extractMetadata_withIsbn13_returnsIsbn13() throws IOException { + String fb2Content = createFb2WithPublishInfo(null, null, "9781234567890"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals("9781234567890", result.getIsbn13()); + } + + @Test + @DisplayName("Should extract ISBN-10 from publish-info") + void extractMetadata_withIsbn10_returnsIsbn10() throws IOException { + String fb2Content = createFb2WithPublishInfo(null, null, "1234567890"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertEquals("1234567890", result.getIsbn10()); + } + } + + @Nested + @DisplayName("Keywords Extraction Tests") + class KeywordsTests { + + @Test + @DisplayName("Should extract keywords as categories") + void extractMetadata_withKeywords_returnsCategories() throws IOException { + String fb2Content = createFb2WithKeywords("adventure, mystery, thriller"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getCategories()); + assertTrue(result.getCategories().contains("adventure")); + assertTrue(result.getCategories().contains("mystery")); + assertTrue(result.getCategories().contains("thriller")); + } + + @Test + @DisplayName("Should handle keywords with semicolon separator") + void extractMetadata_withSemicolonKeywords_returnsCategories() throws IOException { + String fb2Content = createFb2WithKeywords("adventure; mystery; thriller"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertNotNull(result.getCategories()); + assertTrue(result.getCategories().contains("adventure")); + assertTrue(result.getCategories().contains("mystery")); + assertTrue(result.getCategories().contains("thriller")); + } + } + + @Nested + @DisplayName("Author Name Extraction Tests") + class AuthorNameTests { + + @Test + @DisplayName("Should extract author with first and last name") + void extractMetadata_withFirstAndLastName_returnsFullName() throws IOException { + String fb2Content = createFb2WithAuthorNames("John", null, "Doe", null); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertTrue(result.getAuthors().contains("John Doe")); + } + + @Test + @DisplayName("Should extract author with first, middle and last name") + void extractMetadata_withMiddleName_returnsFullNameWithMiddle() throws IOException { + String fb2Content = createFb2WithAuthorNames("John", "Robert", "Doe", null); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertTrue(result.getAuthors().contains("John Robert Doe")); + } + + @Test + @DisplayName("Should use nickname when name parts are missing") + void extractMetadata_withNicknameOnly_returnsNickname() throws IOException { + String fb2Content = createFb2WithAuthorNames(null, null, null, "WriterPro"); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertNotNull(result); + assertTrue(result.getAuthors().contains("WriterPro")); + } + } + + @Nested + @DisplayName("Cover Extraction Tests") + class CoverExtractionTests { + + @Test + @DisplayName("Should extract cover image from binary section") + void extractCover_withCoverImage_returnsCoverBytes() throws IOException { + byte[] imageData = createMinimalPngImage(); + String fb2Content = createFb2WithCover(imageData); + File fb2File = createFb2File(fb2Content); + + byte[] result = extractor.extractCover(fb2File); + + assertNotNull(result); + assertTrue(result.length > 0); + } + + @Test + @DisplayName("Should return null when no cover present") + void extractCover_noCover_returnsNull() throws IOException { + String fb2Content = createMinimalFb2(); + File fb2File = createFb2File(fb2Content); + + byte[] result = extractor.extractCover(fb2File); + + assertNull(result); + } + } + + @Nested + @DisplayName("Complete Metadata Extraction Test") + class CompleteMetadataTest { + + @Test + @DisplayName("Should extract all metadata fields from complete FB2 with title-info") + void extractMetadata_completeFile_extractsAllFields() throws IOException { + String fb2Content = createCompleteFb2(); + File fb2File = createFb2File(fb2Content); + + BookMetadata result = extractor.extractMetadata(fb2File); + + assertAll( + () -> assertNotNull(result, "Metadata should not be null"), + () -> assertEquals("Pride and Prejudice", result.getTitle(), "Title should be extracted"), + () -> assertNotNull(result.getAuthors(), "Authors should not be null"), + () -> assertEquals(1, result.getAuthors().size(), "Should have one author"), + () -> assertTrue(result.getAuthors().contains("Jane Austen"), "Should contain full author name"), + () -> assertNotNull(result.getCategories(), "Categories should not be null"), + () -> assertTrue(result.getCategories().contains("romance"), "Should contain genre"), + () -> assertEquals("en", result.getLanguage(), "Language should be extracted"), + () -> assertNotNull(result.getDescription(), "Description should not be null"), + () -> assertTrue(result.getDescription().contains("classic novel"), "Description should contain annotation text"), + () -> assertEquals(LocalDate.of(1813, 1, 1), result.getPublishedDate(), "Published date should be extracted"), + () -> assertEquals("T. Egerton", result.getPublisher(), "Publisher should be extracted"), + () -> assertEquals("Classic Literature Series", result.getSeriesName(), "Series name should be extracted"), + () -> assertEquals(2.0f, result.getSeriesNumber(), 0.001, "Series number should be extracted") + ); + } + + private String createCompleteFb2() { + return """ + + + + + romance + + Jane + Austen + + Pride and Prejudice + +

Pride and Prejudice is a classic novel by Jane Austen, first published in 1813. It is a romantic novel of manners that follows the character development of Elizabeth Bennet.

+

The novel deals with issues of morality, education, and marriage in the society of the landed gentry of the British Regency. Elizabeth must learn the error of making hasty judgments and come to appreciate the difference between superficial goodness and actual goodness.

+
+ romance, regency, england, bennet, darcy, marriage + 1813 + en + +
+ + + TestUser + + January 1, 2024 + TestUser_PrideAndPrejudice_12345 + 2.0 + + + Pride and Prejudice + T. Egerton + London + 1813 + +
+ +
+ + <p>Chapter 1</p> + +

It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.

+
+ +
+ """; + } + } + + @Nested + @DisplayName("Edge Cases and Error Handling") + class EdgeCaseTests { + + @Test + @DisplayName("Should handle empty FB2 file gracefully") + void extractMetadata_emptyFile_returnsNull() throws IOException { + File emptyFile = tempDir.resolve("empty.fb2").toFile(); + try (FileOutputStream fos = new FileOutputStream(emptyFile)) { + fos.write("".getBytes(StandardCharsets.UTF_8)); + } + + BookMetadata result = extractor.extractMetadata(emptyFile); + + assertNull(result); + } + + @Test + @DisplayName("Should handle invalid XML gracefully") + void extractMetadata_invalidXml_returnsNull() throws IOException { + File invalidFile = tempDir.resolve("invalid.fb2").toFile(); + try (FileOutputStream fos = new FileOutputStream(invalidFile)) { + fos.write("this is not valid XML".getBytes(StandardCharsets.UTF_8)); + } + + BookMetadata result = extractor.extractMetadata(invalidFile); + + assertNull(result); + } + + @Test + @DisplayName("Should handle non-existent file gracefully") + void extractMetadata_nonExistentFile_returnsNull() { + File nonExistent = new File(tempDir.toFile(), "does-not-exist.fb2"); + + BookMetadata result = extractor.extractMetadata(nonExistent); + + assertNull(result); + } + } + + // Helper methods to create FB2 test files + + private String createMinimalFb2() { + return """ + + + + + fiction + + Test + Author + + Test Book + en + + + +
+

Test content

+
+ +
+ """; + } + + private String createFb2WithTitleInfo(String title, String firstName, String lastName, String genre, String lang) { + return String.format(""" + + + + + %s + + %s + %s + + %s + %s + + + +
+

Content

+
+ +
+ """, genre, firstName, lastName, title, lang); + } + + private String createFb2WithMultipleAuthors() { + return """ + + + + + fiction + + Charles + Dickens + + + Jane + Austen + + Collaborative Work + en + + + +
+

Content

+
+ +
+ """; + } + + private String createFb2WithMultipleGenres() { + return """ + + + + + fiction + drama + + Test + Author + + Multi-Genre Book + en + + + +
+

Content

+
+ +
+ """; + } + + private String createFb2WithAnnotation(String annotation) { + return String.format(""" + + + + + fiction + + Test + Author + + Book with Annotation + +

%s

+
+ en +
+
+ +
+

Content

+
+ +
+ """, annotation); + } + + private String createFb2WithDate(String date) { + return String.format(""" + + + + + fiction + + Test + Author + + Book with Date + %s + en + + + +
+

Content

+
+ +
+ """, date); + } + + private String createFb2WithDateValue(String dateValue, String dateText) { + return String.format(""" + + + + + fiction + + Test + Author + + Book with Date Value + %s + en + + + +
+

Content

+
+ +
+ """, dateValue, dateText); + } + + private String createFb2WithSequence(String seriesName, String seriesNumber) { + return String.format(""" + + + + + fiction + + Test + Author + + Book in Series + + en + + + +
+

Content

+
+ +
+ """, seriesName, seriesNumber); + } + + private String createFb2WithPublishInfo(String publisher, String year, String isbn) { + StringBuilder publishInfo = new StringBuilder(); + if (publisher != null) { + publishInfo.append(String.format(" %s\n", publisher)); + } + if (year != null) { + publishInfo.append(String.format(" %s\n", year)); + } + if (isbn != null) { + publishInfo.append(String.format(" %s\n", isbn)); + } + + return String.format(""" + + + + + fiction + + Test + Author + + Book with Publish Info + en + + + %s + + +
+

Content

+
+ +
+ """, publishInfo); + } + + private String createFb2WithKeywords(String keywords) { + return String.format(""" + + + + + fiction + + Test + Author + + Book with Keywords + %s + en + + + +
+

Content

+
+ +
+ """, keywords); + } + + private String createFb2WithAuthorNames(String firstName, String middleName, String lastName, String nickname) { + StringBuilder authorInfo = new StringBuilder(); + if (firstName != null) { + authorInfo.append(String.format(" %s\n", firstName)); + } + if (middleName != null) { + authorInfo.append(String.format(" %s\n", middleName)); + } + if (lastName != null) { + authorInfo.append(String.format(" %s\n", lastName)); + } + if (nickname != null) { + authorInfo.append(String.format(" %s\n", nickname)); + } + + return String.format(""" + + + + + fiction + + %s + Book with Complex Author + en + + + +
+

Content

+
+ +
+ """, authorInfo); + } + + private String createFb2WithCover(byte[] imageData) { + String base64Image = Base64.getEncoder().encodeToString(imageData); + return String.format(""" + + + + + fiction + + Test + Author + + Book with Cover + + + + en + + + +
+

Content

+
+ + %s +
+ """, base64Image); + } + + private byte[] createMinimalPngImage() { + return new byte[]{ + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x01, + 0x08, 0x06, + 0x00, 0x00, 0x00, + (byte) 0x90, (byte) 0x77, (byte) 0x53, (byte) 0xDE, + 0x00, 0x00, 0x00, 0x0A, + 0x49, 0x44, 0x41, 0x54, + 0x78, (byte) 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, + 0x00, 0x01, + 0x0D, (byte) 0x0A, 0x2D, (byte) 0xB4, + 0x00, 0x00, 0x00, 0x00, + 0x49, 0x45, 0x4E, 0x44, + (byte) 0xAE, 0x42, 0x60, (byte) 0x82 + }; + } + + private File createFb2File(String content) throws IOException { + File fb2File = tempDir.resolve("test-" + System.nanoTime() + ".fb2").toFile(); + try (FileOutputStream fos = new FileOutputStream(fb2File)) { + fos.write(content.getBytes(StandardCharsets.UTF_8)); + } + return fb2File; + } +} diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/opds/OpdsFeedServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/opds/OpdsFeedServiceTest.java index 84715638..b3675d77 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/opds/OpdsFeedServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/opds/OpdsFeedServiceTest.java @@ -8,6 +8,7 @@ import com.adityachandel.booklore.model.dto.Library; import com.adityachandel.booklore.model.dto.OpdsUserV2; import com.adityachandel.booklore.model.entity.ShelfEntity; import com.adityachandel.booklore.model.enums.BookFileType; +import com.adityachandel.booklore.model.enums.OpdsSortOrder; import com.adityachandel.booklore.service.MagicShelfService; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; @@ -51,6 +52,7 @@ class OpdsFeedServiceTest { OpdsUserV2 v2 = mock(OpdsUserV2.class); when(userDetails.getOpdsUserV2()).thenReturn(v2); when(v2.getUserId()).thenReturn(TEST_USER_ID); + when(v2.getSortOrder()).thenReturn(OpdsSortOrder.RECENT); when(authenticationService.getOpdsUser()).thenReturn(userDetails); return userDetails; } @@ -152,6 +154,7 @@ class OpdsFeedServiceTest { Page page = new PageImpl<>(List.of(book), PageRequest.of(0, 50), 1); when(opdsBookService.getBooksPage(eq(TEST_USER_ID), any(), any(), any(), eq(0), eq(50))).thenReturn(page); + when(opdsBookService.applySortOrder(any(), any())).thenReturn(page); String xml = opdsFeedService.generateCatalogFeed(request); assertThat(xml).contains("Book Title"); @@ -173,6 +176,7 @@ class OpdsFeedServiceTest { Page page = new PageImpl<>(Collections.emptyList(), PageRequest.of(0, 50), 0); when(opdsBookService.getBooksPage(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(page); + when(opdsBookService.applySortOrder(any(), any())).thenReturn(page); String xml = opdsFeedService.generateCatalogFeed(request); assertThat(xml).contains(""); @@ -196,6 +200,7 @@ class OpdsFeedServiceTest { Page page = new PageImpl<>(List.of(book), PageRequest.of(0, 50), 1); when(opdsBookService.getRecentBooksPage(eq(TEST_USER_ID), eq(0), eq(50))).thenReturn(page); + when(opdsBookService.applySortOrder(any(), any())).thenReturn(page); String xml = opdsFeedService.generateRecentFeed(request); assertThat(xml).contains("Recent Book"); @@ -214,6 +219,7 @@ class OpdsFeedServiceTest { Page page = new PageImpl<>(Collections.emptyList(), PageRequest.of(0, 50), 0); when(opdsBookService.getRecentBooksPage(any(), anyInt(), anyInt())).thenReturn(page); + when(opdsBookService.applySortOrder(any(), any())).thenReturn(page); String xml = opdsFeedService.generateRecentFeed(request); assertThat(xml).contains(""); diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/util/FileServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/util/FileServiceTest.java index f47d0ece..c370c131 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/util/FileServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/util/FileServiceTest.java @@ -1,7 +1,11 @@ package com.adityachandel.booklore.util; import com.adityachandel.booklore.config.AppProperties; +import com.adityachandel.booklore.model.dto.settings.AppSettings; +import com.adityachandel.booklore.model.dto.settings.CoverCroppingSettings; import com.adityachandel.booklore.model.entity.BookMetadataEntity; +import com.adityachandel.booklore.repository.BookMetadataRepository; +import com.adityachandel.booklore.service.appsettings.AppSettingService; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -41,6 +45,9 @@ class FileServiceTest { @Mock private AppProperties appProperties; + @Mock + private AppSettingService appSettingService; + private FileService fileService; @TempDir @@ -48,7 +55,17 @@ class FileServiceTest { @BeforeEach void setup() { - fileService = new FileService(appProperties, mock(RestTemplate.class)); // mock RestTemplate for most tests + CoverCroppingSettings coverCroppingSettings = CoverCroppingSettings.builder() + .verticalCroppingEnabled(true) + .horizontalCroppingEnabled(true) + .aspectRatioThreshold(2.5) + .build(); + AppSettings appSettings = AppSettings.builder() + .coverCroppingSettings(coverCroppingSettings) + .build(); + lenient().when(appSettingService.getAppSettings()).thenReturn(appSettings); + + fileService = new FileService(appProperties, mock(RestTemplate.class), appSettingService, mock(BookMetadataRepository.class)); } @Nested @@ -614,6 +631,116 @@ class FileServiceTest { } } + @Nested + @DisplayName("Cover Cropping for Extreme Aspect Ratios") + class CoverCroppingTests { + + @Test + @DisplayName("extremely tall image is cropped when vertical cropping enabled") + void extremelyTallImage_isCropped() throws IOException { + // Create an extremely tall image like a web comic page (ratio > 2.5) + int width = 940; + int height = 11280; // ratio = 12:1 + + BufferedImage tallImage = createTestImage(width, height); + boolean result = fileService.saveCoverImages(tallImage, 100L); + + assertTrue(result); + + BufferedImage savedCover = ImageIO.read( + new File(fileService.getCoverFile(100L))); + + assertNotNull(savedCover); + + // The image should be cropped to approximately 1.5:1 ratio from the top + double savedRatio = (double) savedCover.getHeight() / savedCover.getWidth(); + assertTrue(savedRatio < 3.0, + "Cropped image should have reasonable aspect ratio, was: " + savedRatio); + } + + @Test + @DisplayName("extremely wide image is cropped when horizontal cropping enabled") + void extremelyWideImage_isCropped() throws IOException { + // Create an extremely wide image (ratio > 2.5) + int width = 3000; + int height = 400; // width/height ratio = 7.5:1 + + BufferedImage wideImage = createTestImage(width, height); + boolean result = fileService.saveCoverImages(wideImage, 101L); + + assertTrue(result); + + BufferedImage savedCover = ImageIO.read( + new File(fileService.getCoverFile(101L))); + + assertNotNull(savedCover); + + // The image should be cropped to a more reasonable aspect ratio + double savedRatio = (double) savedCover.getWidth() / savedCover.getHeight(); + assertTrue(savedRatio < 3.0, + "Cropped image should have reasonable aspect ratio, was: " + savedRatio); + } + + @Test + @DisplayName("normal aspect ratio image is not cropped") + void normalAspectRatioImage_isNotCropped() throws IOException { + // Create a normal book cover sized image (ratio ~1.5:1) + int width = 600; + int height = 900; // ratio = 1.5:1 + + BufferedImage normalImage = createTestImage(width, height); + boolean result = fileService.saveCoverImages(normalImage, 102L); + + assertTrue(result); + + BufferedImage savedCover = ImageIO.read( + new File(fileService.getCoverFile(102L))); + + assertNotNull(savedCover); + + // The image should maintain its original aspect ratio + double originalRatio = (double) height / width; + double savedRatio = (double) savedCover.getHeight() / savedCover.getWidth(); + assertEquals(originalRatio, savedRatio, 0.01, + "Normal aspect ratio image should not be cropped"); + } + + @Test + @DisplayName("cropping is disabled when settings are off") + void croppingDisabled_imageNotCropped() throws IOException { + // Reconfigure with cropping disabled + CoverCroppingSettings disabledSettings = CoverCroppingSettings.builder() + .verticalCroppingEnabled(false) + .horizontalCroppingEnabled(false) + .aspectRatioThreshold(2.5) + .build(); + AppSettings appSettings = AppSettings.builder() + .coverCroppingSettings(disabledSettings) + .build(); + when(appSettingService.getAppSettings()).thenReturn(appSettings); + + // Create an extremely tall image + int width = 400; + int height = 4000; // ratio = 10:1 + + BufferedImage tallImage = createTestImage(width, height); + boolean result = fileService.saveCoverImages(tallImage, 103L); + + assertTrue(result); + + BufferedImage savedCover = ImageIO.read( + new File(fileService.getCoverFile(103L))); + + assertNotNull(savedCover); + + // Since the image exceeds max dimensions, it will be scaled, but aspect ratio preserved + double originalRatio = (double) height / width; + double savedRatio = (double) savedCover.getHeight() / savedCover.getWidth(); + assertEquals(originalRatio, savedRatio, 0.01, + "Image should not be cropped when cropping is disabled"); + } + } + @Nested @DisplayName("createThumbnailFromFile") class CreateThumbnailFromFileTests { @@ -823,12 +950,26 @@ class FileServiceTest { @Mock private RestTemplate restTemplate; + @Mock + private AppSettingService appSettingServiceForNetwork; + private FileService fileService; @BeforeEach void setup() { lenient().when(appProperties.getPathConfig()).thenReturn(tempDir.toString()); - fileService = new FileService(appProperties, restTemplate); + + CoverCroppingSettings coverCroppingSettings = CoverCroppingSettings.builder() + .verticalCroppingEnabled(true) + .horizontalCroppingEnabled(true) + .aspectRatioThreshold(2.5) + .build(); + AppSettings appSettings = AppSettings.builder() + .coverCroppingSettings(coverCroppingSettings) + .build(); + lenient().when(appSettingServiceForNetwork.getAppSettings()).thenReturn(appSettings); + + fileService = new FileService(appProperties, restTemplate, appSettingServiceForNetwork, mock(BookMetadataRepository.class)); } @Nested @@ -844,7 +985,8 @@ class FileServiceTest { byte[] imageBytes = imageToBytes(testImage); RestTemplate mockRestTemplate = mock(RestTemplate.class); - FileService testFileService = new FileService(appProperties, mockRestTemplate); + AppSettingService mockAppSettingService = mock(AppSettingService.class); + FileService testFileService = new FileService(appProperties, mockRestTemplate, mockAppSettingService, mock(BookMetadataRepository.class)); ResponseEntity responseEntity = ResponseEntity.ok(imageBytes); when(mockRestTemplate.exchange( diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/util/PathPatternResolverTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/util/PathPatternResolverTest.java index 1259bb75..c4aa64f1 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/util/PathPatternResolverTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/util/PathPatternResolverTest.java @@ -510,4 +510,104 @@ class PathPatternResolverTest { assertTrue(components[0].equals("Author")); assertTrue(components[1].equals("Series")); } -} + + @Test + @DisplayName("Should preserve extension for files with numeric patterns in name (e.g., Chapter 8.1.cbz)") + void testResolvePattern_filenameWithNumericPattern() { + BookMetadata metadata = BookMetadata.builder() + .title("Comic Title") + .seriesName("Series Name") + .seriesNumber(8.1f) + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{series} - Chapter {seriesIndex}", "original.cbz"); + + assertEquals("Series Name - Chapter 8.1.cbz", result, "Extension should be preserved for files with numeric patterns"); + } + + @Test + @DisplayName("Should preserve extension for files with multiple dots in name") + void testResolvePattern_filenameWithMultipleDots() { + BookMetadata metadata = BookMetadata.builder() + .title("My.Awesome.Book") + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{title}", "My.Awesome.Book.epub"); + + assertEquals("My.Awesome.Book.epub", result, "Extension should be preserved for files with dots in title"); + } + + @Test + @DisplayName("Should add extension when pattern doesn't include it") + void testResolvePattern_extensionNotInPattern() { + BookMetadata metadata = BookMetadata.builder() + .title("Book Title") + .authors(Set.of("Author Name")) + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{authors} - {title}", "original.pdf"); + + assertEquals("Author Name - Book Title.pdf", result, "Extension should be added automatically"); + } + + @Test + @DisplayName("Should not add extension when using {currentFilename} in subdirectory") + void testResolvePattern_currentFilenameWithPath() { + BookMetadata metadata = BookMetadata.builder() + .title("Book Title") + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "books/{currentFilename}", "My.File.With.Dots.epub"); + + assertEquals("books/My.File.With.Dots.epub", result, "Extension should not be added when {currentFilename} is used, even with dots in name"); + } + + @Test + @DisplayName("Should handle title with dots and numeric suffix without duplicating extension") + void testResolvePattern_titleWithDotsAndNumericSuffix() { + BookMetadata metadata = BookMetadata.builder() + .title("Chapter.8.1") + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{title}", "Chapter.8.1.cbz"); + + assertEquals("Chapter.8.1.cbz", result, "Should not treat .1 as extension"); + } + + @Test + @DisplayName("Should preserve CBZ extension for comic files with chapter numbers") + void testResolvePattern_comicWithChapterNumber() { + BookMetadata metadata = BookMetadata.builder() + .seriesName("One Punch Man") + .seriesNumber(8.1f) + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{series} - Chapter {seriesIndex}", "One Punch Man - Chapter 8.1.cbz"); + + assertEquals("One Punch Man - Chapter 8.1.cbz", result, "CBZ extension should be preserved for comics"); + } + + @Test + @DisplayName("Should handle files with only numeric extension-like pattern correctly") + void testResolvePattern_numericExtensionLikePattern() { + BookMetadata metadata = BookMetadata.builder() + .title("Book Version 2") + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{title}.1", "original.epub"); + + assertEquals("Book Version 2.1.epub", result, "Should add real extension even when pattern ends with .1"); + } + + @Test + @DisplayName("Should handle empty extension gracefully") + void testResolvePattern_noExtension() { + BookMetadata metadata = BookMetadata.builder() + .title("Book Title") + .build(); + + String result = PathPatternResolver.resolvePattern(metadata, "{title}", "fileWithoutExtension"); + + assertEquals("Book Title", result, "Should not add extension when original file has none"); + } +} \ No newline at end of file diff --git a/booklore-ui/angular.json b/booklore-ui/angular.json index f6fe3f08..e1b8b338 100644 --- a/booklore-ui/angular.json +++ b/booklore-ui/angular.json @@ -22,7 +22,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": "dist/booklore", "index": "src/index.html", @@ -91,7 +91,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "booklore:build:production" @@ -103,10 +103,10 @@ "defaultConfiguration": "development" }, "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n" + "builder": "@angular/build:extract-i18n" }, "test": { - "builder": "@angular-devkit/build-angular:karma", + "builder": "@angular/build:karma", "options": { "polyfills": [ "zone.js", diff --git a/booklore-ui/package-lock.json b/booklore-ui/package-lock.json index a09c0c33..1cbf2d41 100644 --- a/booklore-ui/package-lock.json +++ b/booklore-ui/package-lock.json @@ -8,17 +8,17 @@ "name": "booklore", "version": "0.0.0", "dependencies": { - "@angular/animations": "^20.3.5", - "@angular/cdk": "^20.2.9", - "@angular/common": "^20.3.5", - "@angular/compiler": "^20.3.5", - "@angular/core": "^20.3.5", - "@angular/forms": "^20.3.5", - "@angular/platform-browser": "^20.3.5", - "@angular/platform-browser-dynamic": "^20.3.5", - "@angular/router": "^20.3.5", + "@angular/animations": "^21.0.5", + "@angular/cdk": "^21.0.3", + "@angular/common": "^21.0.5", + "@angular/compiler": "^21.0.5", + "@angular/core": "^21.0.5", + "@angular/forms": "^21.0.5", + "@angular/platform-browser": "^21.0.5", + "@angular/platform-browser-dynamic": "^21.0.5", + "@angular/router": "^21.0.5", "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@primeng/themes": "^20.4.0", + "@primeng/themes": "^21.0.2", "@stomp/rx-stomp": "^2.3.0", "@stomp/stompjs": "^7.2.1", "@tweenjs/tween.js": "^25.0.0", @@ -30,10 +30,10 @@ "jwt-decode": "^4.0.0", "ng-lazyload-image": "^9.1.3", "ng2-charts": "^8.0.0", - "ngx-extended-pdf-viewer": "^25.6.1", - "ngx-infinite-scroll": "^20.0.0", + "ngx-extended-pdf-viewer": "^25.6.4", + "ngx-infinite-scroll": "^21.0.0", "primeicons": "^7.0.0", - "primeng": "^20.4.0", + "primeng": "^21.0.2", "quill": "^2.0.3", "rxjs": "^7.8.2", "showdown": "^2.1.0", @@ -43,16 +43,16 @@ "zone.js": "^0.16.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^20.3.5", - "@angular/cli": "^20.3.5", - "@angular/compiler-cli": "^20.3.5", + "@angular/build": "^21.0.3", + "@angular/cli": "^21.0.3", + "@angular/compiler-cli": "^21.0.5", "@tailwindcss/typography": "^0.5.19", "@types/jasmine": "^5.1.13", "@types/showdown": "^2.0.6", - "angular-eslint": "^20.3.5", - "autoprefixer": "^10.4.22", - "eslint": "^9.39.1", - "jasmine-core": "^5.12.1", + "angular-eslint": "^21.1.0", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.2", + "jasmine-core": "^5.13.0", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-coverage": "^2.2.1", @@ -60,61 +60,61 @@ "karma-jasmine-html-reporter": "^2.1.0", "tailwindcss": "^3.4.17", "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0" + "typescript-eslint": "^8.50.0" } }, "node_modules/@algolia/abtesting": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.1.0.tgz", - "integrity": "sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.6.1.tgz", + "integrity": "sha512-wV/gNRkzb7sI9vs1OneG129hwe3Q5zPj7zigz3Ps7M5Lpo2hSorrOnXNodHEOV+yXE/ks4Pd+G3CDFIjFTWhMQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-abtesting": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.35.0.tgz", - "integrity": "sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.40.1.tgz", + "integrity": "sha512-cxKNATPY5t+Mv8XAVTI57altkaPH+DZi4uMrnexPxPHODMljhGYY+GDZyHwv9a+8CbZHcY372OkxXrDMZA4Lnw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.35.0.tgz", - "integrity": "sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.40.1.tgz", + "integrity": "sha512-XP008aMffJCRGAY8/70t+hyEyvqqV7YKm502VPu0+Ji30oefrTn2al7LXkITz7CK6I4eYXWRhN6NaIUi65F1OA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.35.0.tgz", - "integrity": "sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.40.1.tgz", + "integrity": "sha512-gWfQuQUBtzUboJv/apVGZMoxSaB0M4Imwl1c9Ap+HpCW7V0KhjBddqF2QQt5tJZCOFsfNIgBbZDGsEPaeKUosw==", "dev": true, "license": "MIT", "engines": { @@ -122,151 +122,151 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.35.0.tgz", - "integrity": "sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.40.1.tgz", + "integrity": "sha512-RTLjST/t+lsLMouQ4zeLJq2Ss+UNkLGyNVu+yWHanx6kQ3LT5jv8UvPwyht9s7R6jCPnlSI77WnL80J32ZuyJg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.35.0.tgz", - "integrity": "sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.40.1.tgz", + "integrity": "sha512-2FEK6bUomBzEYkTKzD0iRs7Ljtjb45rKK/VSkyHqeJnG+77qx557IeSO0qVFE3SfzapNcoytTofnZum0BQ6r3Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.35.0.tgz", - "integrity": "sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.40.1.tgz", + "integrity": "sha512-Nju4NtxAvXjrV2hHZNLKVJLXjOlW6jAXHef/CwNzk1b2qIrCWDO589ELi5ZHH1uiWYoYyBXDQTtHmhaOVVoyXg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.35.0.tgz", - "integrity": "sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.40.1.tgz", + "integrity": "sha512-Mw6pAUF121MfngQtcUb5quZVqMC68pSYYjCRZkSITC085S3zdk+h/g7i6FxnVdbSU6OztxikSDMh1r7Z+4iPlA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.35.0.tgz", - "integrity": "sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.40.1.tgz", + "integrity": "sha512-z+BPlhs45VURKJIxsR99NNBWpUEEqIgwt10v/fATlNxc4UlXvALdOsWzaFfe89/lbP5Bu4+mbO59nqBC87ZM/g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.35.0.tgz", - "integrity": "sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.40.1.tgz", + "integrity": "sha512-VJMUMbO0wD8Rd2VVV/nlFtLJsOAQvjnVNGkMkspFiFhpBA7s/xJOb+fJvvqwKFUjbKTUA7DjiSi1ljSMYBasXg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.35.0.tgz", - "integrity": "sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.40.1.tgz", + "integrity": "sha512-ehvJLadKVwTp9Scg9NfzVSlBKH34KoWOQNTaN8i1Ac64AnO6iH2apJVSP6GOxssaghZ/s8mFQsDH3QIZoluFHA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/client-common": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.35.0.tgz", - "integrity": "sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.40.1.tgz", + "integrity": "sha512-PbidVsPurUSQIr6X9/7s34mgOMdJnn0i6p+N6Ab+lsNhY5eiu+S33kZEpZwkITYBCIbhzDLOvb7xZD3gDi+USA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.35.0.tgz", - "integrity": "sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.40.1.tgz", + "integrity": "sha512-ThZ5j6uOZCF11fMw9IBkhigjOYdXGXQpj6h4k+T9UkZrF2RlKcPynFzDeRgaLdpYk8Yn3/MnFbwUmib7yxj5Lw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.35.0.tgz", - "integrity": "sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.40.1.tgz", + "integrity": "sha512-H1gYPojO6krWHnUXu/T44DrEun/Wl95PJzMXRcM/szstNQczSbwq6wIFJPI9nyE95tarZfUNU3rgorT+wZ6iCQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.35.0" + "@algolia/client-common": "5.40.1" }, "engines": { "node": ">= 14.0.0" @@ -299,13 +299,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2003.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.12.tgz", - "integrity": "sha512-5H40lAFF4CKY32C4HOp6bTlOF1f4WsGCwe7FjFQp9A+T7yoCBiHpIWt2JKTwV4sBoTKVDZOnuf0GG+UVKjQT4A==", + "version": "0.2100.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2100.3.tgz", + "integrity": "sha512-PcruWF0+IxXOTZd9MN/3y4A5aTfblALzT/+zWym26PtisaBgWQ3tRPQsf/CgT8EdmZl8eUOAWlNBSkbUj/S/lQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.12", + "@angular-devkit/core": "21.0.3", "rxjs": "7.8.2" }, "engines": { @@ -314,515 +314,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-angular": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.12.tgz", - "integrity": "sha512-HPepPbJA5vprYTWJaSCfpk0s1bPT6Ui6VjFOSb9oY+p9iq+MGkuB1I+swNcRcMLttyMD+FpbMd27F8jSeX5XVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.12", - "@angular-devkit/build-webpack": "0.2003.12", - "@angular-devkit/core": "20.3.12", - "@angular/build": "20.3.12", - "@babel/core": "7.28.3", - "@babel/generator": "7.28.3", - "@babel/helper-annotate-as-pure": "7.27.3", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-transform-async-generator-functions": "7.28.0", - "@babel/plugin-transform-async-to-generator": "7.27.1", - "@babel/plugin-transform-runtime": "7.28.3", - "@babel/preset-env": "7.28.3", - "@babel/runtime": "7.28.3", - "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "20.3.12", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.21", - "babel-loader": "10.0.0", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "13.0.1", - "css-loader": "7.1.2", - "esbuild-wasm": "0.25.9", - "fast-glob": "3.3.3", - "http-proxy-middleware": "3.0.5", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "karma-source-map-support": "1.4.0", - "less": "4.4.0", - "less-loader": "12.3.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.3.1", - "mini-css-extract-plugin": "2.9.4", - "open": "10.2.0", - "ora": "8.2.0", - "picomatch": "4.0.3", - "piscina": "5.1.3", - "postcss": "8.5.6", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.2", - "sass": "1.90.0", - "sass-loader": "16.0.5", - "semver": "7.7.2", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.43.1", - "tree-kill": "1.2.2", - "tslib": "2.8.1", - "webpack": "5.101.2", - "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.2.2", - "webpack-merge": "6.0.1", - "webpack-subresource-integrity": "5.1.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.25.9" - }, - "peerDependencies": { - "@angular/compiler-cli": "^20.0.0", - "@angular/core": "^20.0.0", - "@angular/localize": "^20.0.0", - "@angular/platform-browser": "^20.0.0", - "@angular/platform-server": "^20.0.0", - "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.12", - "@web/test-runner": "^0.20.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0 || ^30.2.0", - "jest-environment-jsdom": "^29.5.0 || ^30.2.0", - "karma": "^6.3.0", - "ng-packagr": "^20.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "typescript": ">=5.8 <6.0" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@angular/localize": { - "optional": true - }, - "@angular/platform-browser": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/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/@angular-devkit/build-angular/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/less": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.4.0.tgz", - "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/make-dir/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/sass": { - "version": "1.90.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz", - "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack": { - "version": "5.101.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.2.tgz", - "integrity": "sha512-4JLXU0tD6OZNVqlwzm3HGEhAHufSiyv+skb7q0d2367VDMzrU1Q/ZeepvkcHH0rZie6uqEtTQQe0OEOOluH3Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.2003.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2003.12.tgz", - "integrity": "sha512-IkhCU0nAsXYBQOfHu2gQBcYBKhaV1c8wYtu7MmelBcN/iUrG8hRf1sZx+ppUgsdZuBYxCiDiLpcfRVRCIASkvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.2003.12", - "rxjs": "7.8.2" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" - } - }, "node_modules/@angular-devkit/core": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.12.tgz", - "integrity": "sha512-ReFxd/UOoVDr3+kIUjmYILQZF89qg62POdY7a7OqBH7plmInFlYVSEDouJvGqj3LVCPiqTk2ZOSChbhS/eLxXA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.0.3.tgz", + "integrity": "sha512-X1y3GMYru9+Vt7vz+R8SFAEmDtgf0aZ+1JOpiE7ubHsQOnhA++Pb94HBjQ6CHqlUhQli/XPOBksKNdZkpup8rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -848,16 +343,16 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.12.tgz", - "integrity": "sha512-JqJ1u59y+Ud51k/8MHYzSP+aQOeC2PJBaDmMnvqfWVaIt6n3x4gc/VtuhqhpJ0SKulbFuOWgAfI6QbPFrgUYQQ==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.0.3.tgz", + "integrity": "sha512-E/Nja+RIyMzjqLXREOnTRwv7GMrycpAD7kGwDg7l8cWrNQ7phqBZcXAt74Jv9K9aYsOC8tw2Ms9t59aQ6iow8w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.12", + "@angular-devkit/core": "21.0.3", "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", - "ora": "8.2.0", + "magic-string": "0.30.19", + "ora": "9.0.0", "rxjs": "7.8.2" }, "engines": { @@ -867,36 +362,37 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-20.7.0.tgz", - "integrity": "sha512-qgf4Cfs1z0VsVpzF/OnxDRvBp60OIzeCsp4mzlckWYVniKo19EPIN6kFDol5eTAIOMPgiBQlMIwgQMHgocXEig==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.1.0.tgz", + "integrity": "sha512-pcUlDkGqeZ+oQC0oEjnkDDlB96gbgHQhnBUKdhYAiAOSuiBod4+npP0xQOq5chYtRNPBprhDqgrJrp5DBeDMOA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": ">= 0.2000.0 < 0.2100.0", - "@angular-devkit/core": ">= 20.0.0 < 21.0.0" + "@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/bundled-angular-compiler": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-20.7.0.tgz", - "integrity": "sha512-9KPz24YoiL0SvTtTX6sd1zmysU5cKOCcmpEiXkCoO3L2oYZGlVxmMT4hfSaHMt8qmfvV2KzQMoR6DZM84BwRzQ==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.1.0.tgz", + "integrity": "sha512-t52J6FszgEHaJ+IjuzU9qaWfVxsjlVNkAP+B5z2t4NDgbbDDsmI+QJh0OtP1qdlqzjh2pbocEml30KhYmNZm/Q==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-20.7.0.tgz", - "integrity": "sha512-aHH2YTiaonojsKN+y2z4IMugCwdsH/dYIjYBig6kfoSPyf9rGK4zx+gnNGq/pGRjF3bOYrmFgIviYpQVb80inQ==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.1.0.tgz", + "integrity": "sha512-oNp+4UzN2M3KwGwEw03NUdXz93vqJd9sMzTbGXWF9+KVfA2LjckGDTrI6g6asGcJMdyTo07rDcnw0m0MkLB5VA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "20.7.0", - "@angular-eslint/utils": "20.7.0", + "@angular-eslint/bundled-angular-compiler": "21.1.0", + "@angular-eslint/utils": "21.1.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { @@ -906,19 +402,19 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-20.7.0.tgz", - "integrity": "sha512-WFmvW2vBR6ExsSKEaActQTteyw6ikWyuJau9XmWEPFd+2eusEt/+wO21ybjDn3uc5FTp1IcdhfYy+U5OdDjH5w==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.1.0.tgz", + "integrity": "sha512-FlbRfOCn8IUHvP1ebcCSQFVNh+4X/HqZqL7SW5oj9WIYPiOX9ijS03ndNbfX/pBPSIi8GHLKMjLt8zIy1l5Lww==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "20.7.0", - "@angular-eslint/utils": "20.7.0", + "@angular-eslint/bundled-angular-compiler": "21.1.0", + "@angular-eslint/utils": "21.1.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "20.7.0", + "@angular-eslint/template-parser": "21.1.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", @@ -926,29 +422,32 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-20.7.0.tgz", - "integrity": "sha512-S0onfRipDUIL6gFGTFjiWwUDhi42XYrBoi3kJ3wBbKBeIgYv9SP1ppTKDD4ZoDaDU9cQE8nToX7iPn9ifMw6eQ==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.1.0.tgz", + "integrity": "sha512-Hal1mYwx4MTjCcNHqfIlua31xrk2tZJoyTiXiGQ21cAeK4sFuY+9V7/8cxbwJMGftX0G4J7uhx8woOdIFuqiZw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": ">= 20.0.0 < 21.0.0", - "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", - "@angular-eslint/eslint-plugin": "20.7.0", - "@angular-eslint/eslint-plugin-template": "20.7.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/eslint-plugin": "21.1.0", + "@angular-eslint/eslint-plugin-template": "21.1.0", "ignore": "7.0.5", "semver": "7.7.3", "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "@angular/cli": ">= 21.0.0 < 22.0.0" } }, "node_modules/@angular-eslint/template-parser": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-20.7.0.tgz", - "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.1.0.tgz", + "integrity": "sha512-PYVgNbjNtuD5/QOuS6cHR8A7bRqsVqxtUUXGqdv76FYMAajQcAvyfR0QxOkqf3NmYxgNgO3hlUHWq0ILjVbcow==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "20.7.0", + "@angular-eslint/bundled-angular-compiler": "21.1.0", "eslint-scope": "^9.0.0" }, "peerDependencies": { @@ -957,13 +456,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-20.7.0.tgz", - "integrity": "sha512-B6EJHbsk2W/lnS3kS/gm56VGvX735419z/DzgbRDcOvqMGMLwD1ILzv5OTEcL1rzpnB0AHW+IxOu6y/aCzSNUA==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.1.0.tgz", + "integrity": "sha512-rWINgxGREu+NFUPCpAVsBGG8B4hfXxyswM0N5GbjykvsfB5W6PUix2Gsoh++iEsZPT+c9lvgXL5GbpwfanjOow==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "20.7.0" + "@angular-eslint/bundled-angular-compiler": "21.1.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -972,9 +471,9 @@ } }, "node_modules/@angular/animations": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.14.tgz", - "integrity": "sha512-Sx3/XNu2rR+R8T8JkJEaIpZDZPk0IecS0Ayt6HTanNUZXuw0HVou3vkjR5B2St5nM4MXs0gh+S6aLNuArtqJTQ==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.5.tgz", + "integrity": "sha512-7Lr60wLlYcGG+VDnnOY9xpn8Zz3yyJcWGSjNEbXPEGaaD0nTZLNZ1nIXRhTeYZwosK5GvPDFxq68kdLxczskHA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -983,41 +482,42 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.3.14" + "@angular/core": "21.0.5" } }, "node_modules/@angular/build": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.12.tgz", - "integrity": "sha512-iAZve4VPviC8y6RFctyh3qFXSlP5mth9K46/0zasB4LV4pcmu8BrzIHERxIn/jCDNdVdPh973kxo1ksO4WpyuA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.0.3.tgz", + "integrity": "sha512-3h2s0Igruei1RB/Hmu7nwbKvjJQ2ykNaiicXYuS2muWUBhDg+lm0QsGTGXrQV2BD0M9YdHU4Byh9upiZgMYpjA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.12", - "@babel/core": "7.28.3", + "@angular-devkit/architect": "0.2100.3", + "@babel/core": "7.28.4", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.14", + "@inquirer/confirm": "5.1.19", "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", - "browserslist": "^4.23.0", - "esbuild": "0.25.9", + "browserslist": "^4.26.0", + "esbuild": "0.26.0", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", - "listr2": "9.0.1", - "magic-string": "0.30.17", + "listr2": "9.0.5", + "magic-string": "0.30.19", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", "picomatch": "4.0.3", "piscina": "5.1.3", - "rollup": "4.52.3", - "sass": "1.90.0", - "semver": "7.7.2", + "rolldown": "1.0.0-beta.47", + "sass": "1.93.2", + "semver": "7.7.3", "source-map-support": "0.5.21", - "tinyglobby": "0.2.14", - "vite": "7.1.11", + "tinyglobby": "0.2.15", + "undici": "7.16.0", + "vite": "7.2.2", "watchpack": "2.4.4" }, "engines": { @@ -1026,25 +526,25 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.4.2" + "lmdb": "3.4.3" }, "peerDependencies": { - "@angular/compiler": "^20.0.0", - "@angular/compiler-cli": "^20.0.0", - "@angular/core": "^20.0.0", - "@angular/localize": "^20.0.0", - "@angular/platform-browser": "^20.0.0", - "@angular/platform-server": "^20.0.0", - "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.12", + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.0.3", "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^20.0.0", + "ng-packagr": "^21.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.8 <6.0", - "vitest": "^3.1.1" + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" }, "peerDependenciesMeta": { "@angular/core": { @@ -1085,145 +585,47 @@ } } }, - "node_modules/@angular/build/node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/build/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular/build/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/@angular/build/node_modules/sass": { - "version": "1.90.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz", - "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/@angular/build/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular/build/node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, "node_modules/@angular/cdk": { - "version": "20.2.14", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz", - "integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.3.tgz", + "integrity": "sha512-abfckeZfFvovdpxuQHRE4gS1VLNa05Dx0ZSKLGVL9DsQsi4pgn6wWg1y9TkXMlmtpG/EhLmCBxUc6LOHfdeWQA==", "license": "MIT", "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": "^20.0.0 || ^21.0.0", - "@angular/core": "^20.0.0 || ^21.0.0", + "@angular/common": "^21.0.0 || ^22.0.0", + "@angular/core": "^21.0.0 || ^22.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.12.tgz", - "integrity": "sha512-vqVyVjbFPCRMjA5evL7tV2JeR6Anuzb9WcXTMB17fr7uzKNNAvo7KyRaOJjp+TU4JDARTNyGPy0aywfPx7R60A==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.3.tgz", + "integrity": "sha512-3lMR3J231JhLgAt37yEULSHFte3zPeta9VYpIIf92JiBsTnWrvKnaK8RXhfdiSQrvhqQ9FMQdl5AG62r1c4dbA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2003.12", - "@angular-devkit/core": "20.3.12", - "@angular-devkit/schematics": "20.3.12", - "@inquirer/prompts": "7.8.2", - "@listr2/prompt-adapter-inquirer": "3.0.1", - "@modelcontextprotocol/sdk": "1.17.3", - "@schematics/angular": "20.3.12", + "@angular-devkit/architect": "0.2100.3", + "@angular-devkit/core": "21.0.3", + "@angular-devkit/schematics": "21.0.3", + "@inquirer/prompts": "7.9.0", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.24.0", + "@schematics/angular": "21.0.3", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.35.0", + "algoliasearch": "5.40.1", "ini": "5.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.1", - "npm-package-arg": "13.0.0", - "pacote": "21.0.0", - "resolve": "1.22.10", - "semver": "7.7.2", + "listr2": "9.0.5", + "npm-package-arg": "13.0.1", + "pacote": "21.0.3", + "parse5-html-rewriting-stream": "8.0.0", + "resolve": "1.22.11", + "semver": "7.7.3", "yargs": "18.0.0", - "zod": "3.25.76" + "zod": "4.1.13" }, "bin": { "ng": "bin/ng.js" @@ -1234,44 +636,10 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@angular/cli/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@angular/common": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.14.tgz", - "integrity": "sha512-OOUvjTtnpktJLsNupA+GFT2q5zNocPdpOENA8aSrXvAheNybLjgi+otO3U3sQsvB1VwaoEZ9GT5O3lZlstnA/A==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.5.tgz", + "integrity": "sha512-/ZI11F6Wxr8TZRVO4O7pmhBJ9YxDg9mvA76e0PiivmqZggM02HY0y3XPMP3hAOe4K+PfaVBgMAu3P9t32klzfA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1280,14 +648,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "20.3.14", + "@angular/core": "21.0.5", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.14.tgz", - "integrity": "sha512-KFbfPPAbclzGDujCVruflCD9j4Zwwxvrg7Y4C9GJYs3LZ85t+BfIMDDnvpBUM07ZLnfY4TO4gQdHmJAcaGGXDQ==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.5.tgz", + "integrity": "sha512-92sv9pVm9o/8KfPM7T8j5VQmTaSOqmIajrJF8evXE2dNJcwkBpVtzZUqDzr23AV3vg94C7eYU64i8qrsmJ+cYQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1297,13 +665,13 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.14.tgz", - "integrity": "sha512-lFg9ikwRClzDPjdFiwynbVFIi1RJZf/0i+OHa3Ns2gzXxJeHNKMJrHHjWZ2DU4N2UpxH0YAPe22N9Bie28IuQQ==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.0.5.tgz", + "integrity": "sha512-45sFKqt+badXl6Ab2XsxuOsdi0BbIZgcc9TdwmFPdXMNfcSUYDcPiOA0l1iPwDIZiu4VyqzepMfnHB9IwCatgA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.28.3", + "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", @@ -1320,8 +688,8 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.14", - "typescript": ">=5.8 <6.0" + "@angular/compiler": "21.0.5", + "typescript": ">=5.9 <6.0" }, "peerDependenciesMeta": { "typescript": { @@ -1329,58 +697,10 @@ } } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/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/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@angular/core": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.14.tgz", - "integrity": "sha512-rpyEbhWF6Fj/xI9IvNLZh5QBUYnoXuF7vX54CCtyQ2MHALxRR/aa1WRxjRM96cF2OqodQ/Gj3oYW8ei8hlBh4w==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.5.tgz", + "integrity": "sha512-HFXfO5YsBVM+IEaU8h3DZSxO98yDZM2v49NlSVNDzFD3fhnkpTmcgT2NKz9ulIiuV9N376itt+x+NG12sg/+Fw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1389,9 +709,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "20.3.14", + "@angular/compiler": "21.0.5", "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" + "zone.js": "~0.15.0 || ~0.16.0" }, "peerDependenciesMeta": { "@angular/compiler": { @@ -1403,9 +723,9 @@ } }, "node_modules/@angular/forms": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.14.tgz", - "integrity": "sha512-fGrJ589tU+AKoxf+kaRrEw7wlSfVr1/z/Fz625ggFCc6ySQEityKW3JsnLfNkh5qGrdxib4BOfF78f9J7Pyk+w==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.5.tgz", + "integrity": "sha512-RcmXs/LgKyc7D70xVT+3aK/H2SCFEyuebAiw72Iz1te1Gbql2GDFF6hgEOaNwOUglDg8ogN5MdVif2DbRLD3Hw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1414,16 +734,17 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.14", - "@angular/core": "20.3.14", - "@angular/platform-browser": "20.3.14", + "@angular/common": "21.0.5", + "@angular/core": "21.0.5", + "@angular/platform-browser": "21.0.5", + "@standard-schema/spec": "^1.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.14.tgz", - "integrity": "sha512-Lviz9GfsIyOIBDal8QhIBKU8OMH29A0RhFw2opTC50sqKadXLN9CD7iSaAwQbNLc4mc3JAF4zth0AzKdHLbz7Q==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.5.tgz", + "integrity": "sha512-UVCrqOxFmX6kAG3Y6jqjCWvLoTP7fxeY96AsxTMp1fkBdqbQbEPleWQpwngNimsuUPvf+rA6XOxsqiDmRex5mA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1432,9 +753,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "20.3.14", - "@angular/common": "20.3.14", - "@angular/core": "20.3.14" + "@angular/animations": "21.0.5", + "@angular/common": "21.0.5", + "@angular/core": "21.0.5" }, "peerDependenciesMeta": { "@angular/animations": { @@ -1443,9 +764,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.14.tgz", - "integrity": "sha512-g9z/g8gIOrBCX1SQ/GWwB0+JXBC6CKe0+yRyy9GGeBLm/YXWZHxTkmnDmueXXfPtUl8TOAInE22wlLcfunWTrg==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.0.5.tgz", + "integrity": "sha512-0P5vFSS6UhiU7IBeVqPEKmRhMtyQqyXGN9+zF7kLK8H0cx1j0eGVmHRsVuY2YKoVp97fXDIeVGSbO0t5ZcFhoA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1454,16 +775,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.14", - "@angular/compiler": "20.3.14", - "@angular/core": "20.3.14", - "@angular/platform-browser": "20.3.14" + "@angular/common": "21.0.5", + "@angular/compiler": "21.0.5", + "@angular/core": "21.0.5", + "@angular/platform-browser": "21.0.5" } }, "node_modules/@angular/router": { - "version": "20.3.14", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.14.tgz", - "integrity": "sha512-gi7/NuHRS9n9RCwh03VuVFizVMa2lKL/s+7yP3Ecq2nQ5uSeTMWb/91OmGEBwncI3wKPkYdQ9g3n6PvK/O8uDQ==", + "version": "21.0.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.5.tgz", + "integrity": "sha512-IFmf0Wd7jSOoZ8TI+4RXMsYmnIfHQG+kGxeMQVKrefTdr3uEHW/TEsNzbW5bkCpVJHRm4EhkH4hSu8D8tUQffQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1472,9 +793,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "20.3.14", - "@angular/core": "20.3.14", - "@angular/platform-browser": "20.3.14", + "@angular/common": "21.0.5", + "@angular/core": "21.0.5", + "@angular/platform-browser": "21.0.5", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1608,83 +929,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -1695,20 +939,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -1741,79 +971,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", @@ -1857,21 +1014,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", @@ -1902,1159 +1044,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", - "integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", - "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", - "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", - "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", - "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", - "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -3130,20 +1119,44 @@ "node": ">=0.1.90" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.17.0" + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": 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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", - "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.26.0.tgz", + "integrity": "sha512-hj0sKNCQOOo2fgyII3clmJXP28VhgDfU5iy3GNHlWO76KG6N7x4D9ezH5lJtQTG+1J6MFDAJXC1qsI+W+LvZoA==", "cpu": [ "ppc64" ], @@ -3158,9 +1171,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", - "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.26.0.tgz", + "integrity": "sha512-C0hkDsYNHZkBtPxxDx177JN90/1MiCpvBNjz1f5yWJo1+5+c5zr8apjastpEG+wtPjo9FFtGG7owSsAxyKiHxA==", "cpu": [ "arm" ], @@ -3175,9 +1188,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", - "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.26.0.tgz", + "integrity": "sha512-DDnoJ5eoa13L8zPh87PUlRd/IyFaIKOlRbxiwcSbeumcJ7UZKdtuMCHa1Q27LWQggug6W4m28i4/O2qiQQ5NZQ==", "cpu": [ "arm64" ], @@ -3192,9 +1205,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", - "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.26.0.tgz", + "integrity": "sha512-bKDkGXGZnj0T70cRpgmv549x38Vr2O3UWLbjT2qmIkdIWcmlg8yebcFWoT9Dku7b5OV3UqPEuNKRzlNhjwUJ9A==", "cpu": [ "x64" ], @@ -3209,9 +1222,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", - "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.26.0.tgz", + "integrity": "sha512-6Z3naJgOuAIB0RLlJkYc81An3rTlQ/IeRdrU3dOea8h/PvZSgitZV+thNuIccw0MuK1GmIAnAmd5TrMZad8FTQ==", "cpu": [ "arm64" ], @@ -3226,9 +1239,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", - "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.26.0.tgz", + "integrity": "sha512-OPnYj0zpYW0tHusMefyaMvNYQX5pNQuSsHFTHUBNp3vVXupwqpxofcjVsUx11CQhGVkGeXjC3WLjh91hgBG2xw==", "cpu": [ "x64" ], @@ -3243,9 +1256,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", - "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.26.0.tgz", + "integrity": "sha512-jix2fa6GQeZhO1sCKNaNMjfj5hbOvoL2F5t+w6gEPxALumkpOV/wq7oUBMHBn2hY2dOm+mEV/K+xfZy3mrsxNQ==", "cpu": [ "arm64" ], @@ -3260,9 +1273,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", - "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.26.0.tgz", + "integrity": "sha512-tccJaH5xHJD/239LjbVvJwf6T4kSzbk6wPFerF0uwWlkw/u7HL+wnAzAH5GB2irGhYemDgiNTp8wJzhAHQ64oA==", "cpu": [ "x64" ], @@ -3277,9 +1290,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", - "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.26.0.tgz", + "integrity": "sha512-JY8NyU31SyRmRpuc5W8PQarAx4TvuYbyxbPIpHAZdr/0g4iBr8KwQBS4kiiamGl2f42BBecHusYCsyxi7Kn8UQ==", "cpu": [ "arm" ], @@ -3294,9 +1307,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", - "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.26.0.tgz", + "integrity": "sha512-IMJYN7FSkLttYyTbsbme0Ra14cBO5z47kpamo16IwggzzATFY2lcZAwkbcNkWiAduKrTgFJP7fW5cBI7FzcuNQ==", "cpu": [ "arm64" ], @@ -3311,9 +1324,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", - "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.26.0.tgz", + "integrity": "sha512-XITaGqGVLgk8WOHw8We9Z1L0lbLFip8LyQzKYFKO4zFo1PFaaSKsbNjvkb7O8kEXytmSGRkYpE8LLVpPJpsSlw==", "cpu": [ "ia32" ], @@ -3328,9 +1341,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", - "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.26.0.tgz", + "integrity": "sha512-MkggfbDIczStUJwq9wU7gQ7kO33d8j9lWuOCDifN9t47+PeI+9m2QVh51EI/zZQ1spZtFMC1nzBJ+qNGCjJnsg==", "cpu": [ "loong64" ], @@ -3345,9 +1358,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", - "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.26.0.tgz", + "integrity": "sha512-fUYup12HZWAeccNLhQ5HwNBPr4zXCPgUWzEq2Rfw7UwqwfQrFZ0SR/JljaURR8xIh9t+o1lNUFTECUTmaP7yKA==", "cpu": [ "mips64el" ], @@ -3362,9 +1375,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", - "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.26.0.tgz", + "integrity": "sha512-MzRKhM0Ip+//VYwC8tialCiwUQ4G65WfALtJEFyU0GKJzfTYoPBw5XNWf0SLbCUYQbxTKamlVwPmcw4DgZzFxg==", "cpu": [ "ppc64" ], @@ -3379,9 +1392,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", - "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.26.0.tgz", + "integrity": "sha512-QhCc32CwI1I4Jrg1enCv292sm3YJprW8WHHlyxJhae/dVs+KRWkbvz2Nynl5HmZDW/m9ZxrXayHzjzVNvQMGQA==", "cpu": [ "riscv64" ], @@ -3396,9 +1409,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", - "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.26.0.tgz", + "integrity": "sha512-1D6vi6lfI18aNT1aTf2HV+RIlm6fxtlAp8eOJ4mmnbYmZ4boz8zYDar86sIYNh0wmiLJEbW/EocaKAX6Yso2fw==", "cpu": [ "s390x" ], @@ -3413,9 +1426,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", - "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.26.0.tgz", + "integrity": "sha512-rnDcepj7LjrKFvZkx+WrBv6wECeYACcFjdNPvVPojCPJD8nHpb3pv3AuR9CXgdnjH1O23btICj0rsp0L9wAnHA==", "cpu": [ "x64" ], @@ -3430,9 +1443,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", - "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.26.0.tgz", + "integrity": "sha512-FSWmgGp0mDNjEXXFcsf12BmVrb+sZBBBlyh3LwB/B9ac3Kkc8x5D2WimYW9N7SUkolui8JzVnVlWh7ZmjCpnxw==", "cpu": [ "arm64" ], @@ -3447,9 +1460,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", - "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.26.0.tgz", + "integrity": "sha512-0QfciUDFryD39QoSPUDshj4uNEjQhp73+3pbSAaxjV2qGOEDsM67P7KbJq7LzHoVl46oqhIhJ1S+skKGR7lMXA==", "cpu": [ "x64" ], @@ -3464,9 +1477,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", - "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.26.0.tgz", + "integrity": "sha512-vmAK+nHhIZWImwJ3RNw9hX3fU4UGN/OqbSE0imqljNbUQC3GvVJ1jpwYoTfD6mmXmQaxdJY6Hn4jQbLGJKg5Yw==", "cpu": [ "arm64" ], @@ -3481,9 +1494,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", - "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.26.0.tgz", + "integrity": "sha512-GPXF7RMkJ7o9bTyUsnyNtrFMqgM3X+uM/LWw4CeHIjqc32fm0Ir6jKDnWHpj8xHFstgWDUYseSABK9KCkHGnpg==", "cpu": [ "x64" ], @@ -3498,9 +1511,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", - "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.26.0.tgz", + "integrity": "sha512-nUHZ5jEYqbBthbiBksbmHTlbb5eElyVfs/s1iHQ8rLBq1eWsd5maOnDpCocw1OM8kFK747d1Xms8dXJHtduxSw==", "cpu": [ "arm64" ], @@ -3515,9 +1528,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", - "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.26.0.tgz", + "integrity": "sha512-TMg3KCTCYYaVO+R6P5mSORhcNDDlemUVnUbb8QkboUtOhb5JWKAzd5uMIMECJQOxHZ/R+N8HHtDF5ylzLfMiLw==", "cpu": [ "x64" ], @@ -3532,9 +1545,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", - "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.26.0.tgz", + "integrity": "sha512-apqYgoAUd6ZCb9Phcs8zN32q6l0ZQzQBdVXOofa6WvHDlSOhwCWgSfVQabGViThS40Y1NA4SCvQickgZMFZRlA==", "cpu": [ "arm64" ], @@ -3549,9 +1562,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", - "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.26.0.tgz", + "integrity": "sha512-FGJAcImbJNZzLWu7U6WB0iKHl4RuY4TsXEwxJPl9UZLS47agIZuILZEX3Pagfw7I4J3ddflomt9f0apfaJSbaw==", "cpu": [ "ia32" ], @@ -3566,9 +1579,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", - "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.26.0.tgz", + "integrity": "sha512-WAckBKaVnmFqbEhbymrPK7M086DQMpL1XoRbpmN0iW8k5JSXjDRQBhcZNa0VweItknLq9eAeCL34jK7/CDcw7A==", "cpu": [ "x64" ], @@ -3759,9 +1772,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -3895,14 +1908,14 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", - "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.19.tgz", + "integrity": "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", - "@inquirer/type": "^3.0.8" + "@inquirer/core": "^10.3.0", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -4090,22 +2103,22 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.2.tgz", - "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.9.0.tgz", + "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.2.1", - "@inquirer/confirm": "^5.1.14", - "@inquirer/editor": "^4.2.17", - "@inquirer/expand": "^4.0.17", - "@inquirer/input": "^4.2.1", - "@inquirer/number": "^3.0.17", - "@inquirer/password": "^4.0.17", - "@inquirer/rawlist": "^4.1.5", - "@inquirer/search": "^3.1.0", - "@inquirer/select": "^4.3.1" + "@inquirer/checkbox": "^4.3.0", + "@inquirer/confirm": "^5.1.19", + "@inquirer/editor": "^4.2.21", + "@inquirer/expand": "^4.0.21", + "@inquirer/input": "^4.2.5", + "@inquirer/number": "^3.0.21", + "@inquirer/password": "^4.0.21", + "@inquirer/rawlist": "^4.1.9", + "@inquirer/search": "^3.2.0", + "@inquirer/select": "^4.4.0" }, "engines": { "node": ">=18" @@ -4232,80 +2245,6 @@ "node": "20 || >=22" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/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/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -4365,6 +2304,8 @@ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -4386,160 +2327,33 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT" - }, "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz", - "integrity": "sha512-3XFmGwm3u6ioREG+ynAQB7FoxfajgQnMhIu8wC5eo/Lsih4aKDg0VuIMGaOsYn7hJSJagSeaD4K8yfpkEoDEmA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/type": "^3.0.7" + "@inquirer/type": "^3.0.8" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "@inquirer/prompts": ">= 3 < 8", - "listr2": "9.0.1" + "listr2": "9.0.5" } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.2.tgz", - "integrity": "sha512-NK80WwDoODyPaSazKbzd3NEJ3ygePrkERilZshxBViBARNz21rmediktGHExoj9n5t9+ChlgLlxecdFKLCuCKg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.4.3.tgz", + "integrity": "sha512-zR6Y45VNtW5s+A+4AyhrJk0VJKhXdkLhrySCpCu7PSdnakebsOzNxf58p5Xoq66vOSuueGAxlqDAF49HwdrSTQ==", "cpu": [ "arm64" ], @@ -4551,9 +2365,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.2.tgz", - "integrity": "sha512-zevaowQNmrp3U7Fz1s9pls5aIgpKRsKb3dZWDINtLiozh3jZI9fBrI19lYYBxqdyiIyNdlyiidPnwPShj4aK+w==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.4.3.tgz", + "integrity": "sha512-nfGm5pQksBGfaj9uMbjC0YyQreny/Pl7mIDtHtw6g7WQuCgeLullr9FNRsYyKplaEJBPrCVpEjpAznxTBIrXBw==", "cpu": [ "x64" ], @@ -4565,9 +2379,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.2.tgz", - "integrity": "sha512-OmHCULY17rkx/RoCoXlzU7LyR8xqrksgdYWwtYa14l/sseezZ8seKWXcogHcjulBddER5NnEFV4L/Jtr2nyxeg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.4.3.tgz", + "integrity": "sha512-Kjqomp7i0rgSbYSUmv9JnXpS55zYT/YcW3Bdf9oqOTjcH0/8tFAP8MLhu/i9V2pMKIURDZk63Ww49DTK0T3c/Q==", "cpu": [ "arm" ], @@ -4579,9 +2393,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.2.tgz", - "integrity": "sha512-ZBEfbNZdkneebvZs98Lq30jMY8V9IJzckVeigGivV7nTHJc+89Ctomp1kAIWKlwIG0ovCDrFI448GzFPORANYg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.4.3.tgz", + "integrity": "sha512-uX9eaPqWb740wg5D3TCvU/js23lSRSKT7lJrrQ8IuEG/VLgpPlxO3lHDywU44yFYdGS7pElBn6ioKFKhvALZlw==", "cpu": [ "arm64" ], @@ -4593,9 +2407,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.2.tgz", - "integrity": "sha512-vL9nM17C77lohPYE4YaAQvfZCSVJSryE4fXdi8M7uWPBnU+9DJabgKVAeyDb84ZM2vcFseoBE4/AagVtJeRE7g==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.4.3.tgz", + "integrity": "sha512-7/8l20D55CfwdMupkc3fNxNJdn4bHsti2X0cp6PwiXlLeSFvAfWs5kCCx+2Cyje4l4GtN//LtKWjTru/9hDJQg==", "cpu": [ "x64" ], @@ -4607,9 +2421,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.2.tgz", - "integrity": "sha512-SXWjdBfNDze4ZPeLtYIzsIeDJDJ/SdsA0pEXcUBayUIMO0FQBHfVZZyHXQjjHr4cvOAzANBgIiqaXRwfMhzmLw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.4.3.tgz", + "integrity": "sha512-yWVR0e5Gl35EGJBsAuqPOdjtUYuN8CcTLKrqpQFoM+KsMadViVCulhKNhkcjSGJB88Am5bRPjMro4MBB9FS23Q==", "cpu": [ "arm64" ], @@ -4621,9 +2435,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.2.tgz", - "integrity": "sha512-IY+r3bxKW6Q6sIPiMC0L533DEfRJSXibjSI3Ft/w9Q8KQBNqEIvUFXt+09wV8S5BRk0a8uSF19YWxuRwEfI90g==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.4.3.tgz", + "integrity": "sha512-1JdBkcO0Vrua4LUgr4jAe4FUyluwCeq/pDkBrlaVjX3/BBWP1TzVjCL+TibWNQtPAL1BITXPAhlK5Ru4FBd/hg==", "cpu": [ "x64" ], @@ -4635,13 +2449,14 @@ ] }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.17.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", - "integrity": "sha512-JPwUKWSsbzx+DLFznf/QZ32Qa+ptfbUlHhRLrBQBAFu9iI1iYvizM4p+zhhRDceSsPutXp4z+R/HPVphlIiclg==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz", + "integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -4649,39 +2464,28 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5089,21 +2893,17 @@ "node": ">= 10" } }, - "node_modules/@ngtools/webpack": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.12.tgz", - "integrity": "sha512-ePuofHOtbgvEq2t+hcmL30s4q9HQ/nv9ABwpLiELdVIObcWUnrnizAvM7hujve/9CQL6gRCeEkxPLPS4ZrK9AQ==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", + "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", "dev": true, "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^20.0.0", - "typescript": ">=5.8 <6.0", - "webpack": "^5.54.0" + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -5142,60 +2942,86 @@ } }, "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "dev": true, "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", + "lru-cache": "^11.2.1", "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "dev": true, "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", - "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.1.tgz", + "integrity": "sha512-+XTFxK2jJF/EJJ5SoAzXk3qwIDfvFc5/g+bD274LZ7uY7LE8sTfG6Z8rOanPl2ZEvZWqNvmEdtXC25cE54VcoA==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^5.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { @@ -5209,16 +3035,29 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -5228,7 +3067,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/installed-package-contents": { @@ -5249,74 +3088,77 @@ } }, "node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", - "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", - "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.4.tgz", + "integrity": "sha512-0wInJG3j/K40OJt/33ax47WfWMzZTm6OQxB9cDhTt5huCP2a9g2GnlsxmfN+PulItNPIpPrZ+kfwwUil7eHcZQ==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", "semver": "^7.5.3", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/@npmcli/promise-spawn": { "version": "8.0.3", @@ -5358,31 +3200,44 @@ } }, "node_modules/@npmcli/redact": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", - "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", - "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.3.tgz", + "integrity": "sha512-ER2N6itRkzWbbtVmZ9WKaWxVlKlOeBFF1/7xx+KA5J1xKa4JjUwBdb6tDpk0v1qA+d+VDwHI9qmLcXSWcmi+Rw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0", + "which": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script/node_modules/isexe": { @@ -5395,10 +3250,20 @@ "node": ">=16" } }, + "node_modules/@npmcli/run-script/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@npmcli/run-script/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -5408,7 +3273,17 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.96.0.tgz", + "integrity": "sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@parcel/watcher": { @@ -5461,7 +3336,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5483,7 +3357,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5505,7 +3378,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5527,7 +3399,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5549,7 +3420,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5571,7 +3441,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5593,7 +3462,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5615,7 +3483,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5637,7 +3504,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5659,7 +3525,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5681,7 +3546,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5703,7 +3567,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5725,7 +3588,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -5756,25 +3618,26 @@ "license": "MIT", "optional": true }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@primeng/themes": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-20.4.0.tgz", - "integrity": "sha512-bh1yIRbCDAo+OLhQ+bm8sgwlZFRphwlR3/GXOdshJVurm5/Up+CWzoRqsZw/Q2RSrq0x3rDNA2pOTIYpcwgXbA==", + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@primeng/themes/-/themes-21.0.2.tgz", + "integrity": "sha512-wSw8BtrNUZBmgU5umzMj9WQKVyrLRQkAYORnRYI4wD6+TXVb/J2kQLHCA26vHbXfI6pO4fruft9mySFWlzXk0Q==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/styled": "^0.7.4", - "@primeuix/themes": "^1.2.5" + "@primeuix/themes": "^2.0.2" + } + }, + "node_modules/@primeuix/motion": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@primeuix/motion/-/motion-0.0.10.tgz", + "integrity": "sha512-PsZwOPq79Scp7/ionshRcQ5xKVf9+zuLcyY5mf6onK8chHT5C9JGphmcIZ4CzcqxuGEpsm8AIbTGy+zS3RtzLA==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.3" + }, + "engines": { + "node": ">=12.11.0" } }, "node_modules/@primeuix/styled": { @@ -5790,21 +3653,21 @@ } }, "node_modules/@primeuix/styles": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.5.tgz", - "integrity": "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.2.tgz", + "integrity": "sha512-LNtkJsTonNHF5ag+9s3+zQzm00+LRmffw68QRIHy6S/dam1JpdrrAnUzNYlWbaY7aE2EkZvQmx7Np7+PyHn+ow==", "license": "MIT", "dependencies": { - "@primeuix/styled": "^0.7.3" + "@primeuix/styled": "^0.7.4" } }, "node_modules/@primeuix/themes": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-1.2.5.tgz", - "integrity": "sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-2.0.2.tgz", + "integrity": "sha512-prwQvA3tDGBz8yWSUenaJUttEMCEvPvxwOfFhDPmSe1vwsfVKL2Nmh5eZvtPFQnxmIOPsHZS7zc0/L3CzJ83Eg==", "license": "MIT", "dependencies": { - "@primeuix/styled": "^0.7.3" + "@primeuix/styled": "^0.7.4" } }, "node_modules/@primeuix/utils": { @@ -5816,10 +3679,255 @@ "node": ">=12.11.0" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.47.tgz", + "integrity": "sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.47.tgz", + "integrity": "sha512-Lc3nrkxeaDVCVl8qR3qoxh6ltDZfkQ98j5vwIr5ALPkgjZtDK4BGCrrBoLpGVMg+csWcaqUbwbKwH5yvVa0oOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.47.tgz", + "integrity": "sha512-eBYxQDwP0O33plqNVqOtUHqRiSYVneAknviM5XMawke3mwMuVlAsohtOqEjbCEl/Loi/FWdVeks5WkqAkzkYWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.47.tgz", + "integrity": "sha512-Ns+kgp2+1Iq/44bY/Z30DETUSiHY7ZuqaOgD5bHVW++8vme9rdiWsN4yG4rRPXkdgzjvQ9TDHmZZKfY4/G11AA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.47.tgz", + "integrity": "sha512-4PecgWCJhTA2EFOlptYJiNyVP2MrVP4cWdndpOu3WmXqWqZUmSubhb4YUAIxAxnXATlGjC1WjxNPhV7ZllNgdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.47.tgz", + "integrity": "sha512-CyIunZ6D9U9Xg94roQI1INt/bLkOpPsZjZZkiaAZ0r6uccQdICmC99M9RUPlMLw/qg4yEWLlQhG73W/mG437NA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.47.tgz", + "integrity": "sha512-doozc/Goe7qRCSnzfJbFINTHsMktqmZQmweull6hsZZ9sjNWQ6BWQnbvOlfZJe4xE5NxM1NhPnY5Giqnl3ZrYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.47.tgz", + "integrity": "sha512-fodvSMf6Aqwa0wEUSTPewmmZOD44rc5Tpr5p9NkwQ6W1SSpUKzD3SwpJIgANDOhwiYhDuiIaYPGB7Ujkx1q0UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.47.tgz", + "integrity": "sha512-Rxm5hYc0mGjwLh5sjlGmMygxAaV2gnsx7CNm2lsb47oyt5UQyPDZf3GP/ct8BEcwuikdqzsrrlIp8+kCSvMFNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.47.tgz", + "integrity": "sha512-YakuVe+Gc87jjxazBL34hbr8RJpRuFBhun7NEqoChVDlH5FLhLXjAPHqZd990TVGVNkemourf817Z8u2fONS8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.47.tgz", + "integrity": "sha512-ak2GvTFQz3UAOw8cuQq8pWE+TNygQB6O47rMhvevvTzETh7VkHRFtRUwJynX5hwzFvQMP6G0az5JrBGuwaMwYQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.47.tgz", + "integrity": "sha512-o5BpmBnXU+Cj+9+ndMcdKjhZlPb79dVPBZnWwMnI4RlNSSq5yOvFZqvfPYbyacvnW03Na4n5XXQAPhu3RydZ0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.47.tgz", + "integrity": "sha512-FVOmfyYehNE92IfC9Kgs913UerDog2M1m+FADJypKz0gmRg3UyTt4o1cZMCAl7MiR89JpM9jegNO1nXuP1w1vw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.47.tgz", + "integrity": "sha512-by/70F13IUE101Bat0oeH8miwWX5mhMFPk1yjCdxoTNHTyTdLgb0THNaebRM6AP7Kz+O3O2qx87sruYuF5UxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", - "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", "cpu": [ "arm" ], @@ -5831,9 +3939,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", - "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", "cpu": [ "arm64" ], @@ -5845,9 +3953,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", - "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", "cpu": [ "arm64" ], @@ -5859,9 +3967,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", - "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", "cpu": [ "x64" ], @@ -5873,9 +3981,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", - "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", "cpu": [ "arm64" ], @@ -5887,9 +3995,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", - "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", "cpu": [ "x64" ], @@ -5901,9 +4009,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", - "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", "cpu": [ "arm" ], @@ -5915,9 +4023,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", - "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", "cpu": [ "arm" ], @@ -5929,9 +4037,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", - "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", "cpu": [ "arm64" ], @@ -5943,9 +4051,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", - "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", "cpu": [ "arm64" ], @@ -5957,9 +4065,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", - "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", "cpu": [ "loong64" ], @@ -5971,9 +4079,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", - "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", "cpu": [ "ppc64" ], @@ -5985,9 +4093,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", - "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", "cpu": [ "riscv64" ], @@ -5999,9 +4107,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", - "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", "cpu": [ "riscv64" ], @@ -6013,9 +4121,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", - "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", "cpu": [ "s390x" ], @@ -6027,9 +4135,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", - "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", "cpu": [ "x64" ], @@ -6041,9 +4149,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", - "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", "cpu": [ "x64" ], @@ -6055,9 +4163,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", - "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", "cpu": [ "arm64" ], @@ -6069,9 +4177,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", - "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", "cpu": [ "arm64" ], @@ -6083,9 +4191,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", - "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", "cpu": [ "ia32" ], @@ -6097,9 +4205,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", - "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", "cpu": [ "x64" ], @@ -6111,9 +4219,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", - "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", "cpu": [ "x64" ], @@ -6125,14 +4233,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "20.3.12", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.12.tgz", - "integrity": "sha512-ikl+nkWUab/Z4eSkBHgq9FLIUH8qh4OcYKeBQ0fyWqIUFHyjjK0JOfwmH1g/3zAmuUMtkthHCehAtyKzCTQjVA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.0.3.tgz", + "integrity": "sha512-XYOI2WOz8B+ydJ8iUHRXrUyjTx+YGdCQ8b2FlXnU46ksIctVU+zt4Zgu6462xeaPwOFYw6+r+TvaBAZ14a82Gw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.12", - "@angular-devkit/schematics": "20.3.12", + "@angular-devkit/core": "21.0.3", + "@angular-devkit/schematics": "21.0.3", "jsonc-parser": "3.3.1" }, "engines": { @@ -6142,32 +4250,32 @@ } }, "node_modules/@sigstore/bundle": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", - "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", - "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.0.0.tgz", + "integrity": "sha512-NgbJ+aW9gQl/25+GIEGYcCyi8M+ng2/5X04BMuIgoDfgvp18vDcoNHOQjQsG9418HGNYRxG3vfEXaR1ayD37gg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", - "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", + "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6175,50 +4283,50 @@ } }, "node_modules/@sigstore/sign": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", - "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.0.1.tgz", + "integrity": "sha512-KFNGy01gx9Y3IBPG/CergxR9RZpN43N+lt3EozEfeoyqm8vEiLxwRl3ZO5sPx3Obv1ix/p7FWOlPc2Jgwfp9PA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.2", "proc-log": "^5.0.0", "promise-retry": "^2.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/tuf": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", - "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.0.tgz", + "integrity": "sha512-0QFuWDHOQmz7t66gfpfNO6aEjoFrdhkJaej/AOqb4kqWZVbPWFZifXZzkxyQBB1OwTbkhdT3LNpMFxwkTvf+2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.4.1", - "tuf-js": "^3.0.1" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@sigstore/verify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", - "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.0.0.tgz", + "integrity": "sha512-moXtHH33AobOhTZF8xcX1MpOFqdvfCk7v6+teJL8zymBiDXwEsQH6XG9HGx2VIxnJZNm4cNSzflTLDnQLmIdmw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.1" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@socket.io/component-emitter": { @@ -6228,6 +4336,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT", + "peer": true + }, "node_modules/@stomp/rx-stomp": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@stomp/rx-stomp/-/rx-stomp-2.3.0.tgz", @@ -6269,9 +4384,9 @@ } }, "node_modules/@tufjs/models": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", - "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.0.0.tgz", + "integrity": "sha512-h5x5ga/hh82COe+GoD4+gKUeV4T3iaYOxqLt41GRKApinPI7DMidhCmNVTjKfhCWFJIGXaFJee07XczdT4jdZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6279,7 +4394,7 @@ "minimatch": "^9.0.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@tweenjs/tween.js": { @@ -6288,46 +4403,15 @@ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "license": "MIT" }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" + "tslib": "^2.4.0" } }, "node_modules/@types/cors": { @@ -6340,28 +4424,6 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -6369,49 +4431,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/jasmine": { "version": "5.1.13", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.13.tgz", @@ -6436,13 +4455,6 @@ "localforage": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -6453,80 +4465,6 @@ "undici-types": "~7.16.0" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, "node_modules/@types/showdown": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/showdown/-/showdown-2.0.6.tgz", @@ -6534,39 +4472,18 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", - "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/type-utils": "8.48.0", - "@typescript-eslint/utils": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -6579,22 +4496,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.0", + "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.0.tgz", - "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "engines": { @@ -6610,14 +4527,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", - "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.0", - "@typescript-eslint/types": "^8.48.0", + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "engines": { @@ -6632,14 +4549,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", - "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0" + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6650,9 +4567,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", - "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", "dev": true, "license": "MIT", "engines": { @@ -6667,15 +4584,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", - "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6692,9 +4609,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.0.tgz", - "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", "dev": true, "license": "MIT", "engines": { @@ -6706,16 +4623,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", - "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.0", - "@typescript-eslint/tsconfig-utils": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/visitor-keys": "8.48.0", + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -6734,16 +4651,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.0.tgz", - "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.0", - "@typescript-eslint/types": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0" + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6758,13 +4675,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", - "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -6801,167 +4718,6 @@ "vite": "^6.0.0 || ^7.0.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", @@ -6972,20 +4728,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -6994,13 +4736,13 @@ "license": "BSD-2-Clause" }, "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/accepts": { @@ -7030,19 +4772,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -7053,35 +4782,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -7127,63 +4827,51 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/algoliasearch": { - "version": "5.35.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", - "integrity": "sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.40.1.tgz", + "integrity": "sha512-iUNxcXUNg9085TJx0HJLjqtDE0r1RZ0GOGrt8KNQqQT5ugu8lZsHuMUYW/e0lHhq6xBvmktU9Bw4CXP9VQeKrg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/abtesting": "1.1.0", - "@algolia/client-abtesting": "5.35.0", - "@algolia/client-analytics": "5.35.0", - "@algolia/client-common": "5.35.0", - "@algolia/client-insights": "5.35.0", - "@algolia/client-personalization": "5.35.0", - "@algolia/client-query-suggestions": "5.35.0", - "@algolia/client-search": "5.35.0", - "@algolia/ingestion": "1.35.0", - "@algolia/monitoring": "1.35.0", - "@algolia/recommend": "5.35.0", - "@algolia/requester-browser-xhr": "5.35.0", - "@algolia/requester-fetch": "5.35.0", - "@algolia/requester-node-http": "5.35.0" + "@algolia/abtesting": "1.6.1", + "@algolia/client-abtesting": "5.40.1", + "@algolia/client-analytics": "5.40.1", + "@algolia/client-common": "5.40.1", + "@algolia/client-insights": "5.40.1", + "@algolia/client-personalization": "5.40.1", + "@algolia/client-query-suggestions": "5.40.1", + "@algolia/client-search": "5.40.1", + "@algolia/ingestion": "1.40.1", + "@algolia/monitoring": "1.40.1", + "@algolia/recommend": "5.40.1", + "@algolia/requester-browser-xhr": "5.40.1", + "@algolia/requester-fetch": "5.40.1", + "@algolia/requester-node-http": "5.40.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/angular-eslint": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-20.7.0.tgz", - "integrity": "sha512-BCiTCLO3dr8pGPaM7qLcCruWNcoNNHnLn4DPqE5tHk1TAnTx5TcGy0p/FygharZw5RjWfDHLBjFfpeh4XWLMmQ==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.1.0.tgz", + "integrity": "sha512-qXpIEBNYpfgpBaFblnyFegVSQjWCVUdCXTHvMcvtNtmMgtPwIDKvG8wuJo5BbQ/MNt2d8npmnRUaS2ddzdCzww==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": ">= 20.0.0 < 21.0.0", - "@angular-devkit/schematics": ">= 20.0.0 < 21.0.0", - "@angular-eslint/builder": "20.7.0", - "@angular-eslint/eslint-plugin": "20.7.0", - "@angular-eslint/eslint-plugin-template": "20.7.0", - "@angular-eslint/schematics": "20.7.0", - "@angular-eslint/template-parser": "20.7.0", + "@angular-devkit/core": ">= 21.0.0 < 22.0.0", + "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", + "@angular-eslint/builder": "21.1.0", + "@angular-eslint/eslint-plugin": "21.1.0", + "@angular-eslint/eslint-plugin-template": "21.1.0", + "@angular-eslint/schematics": "21.1.0", + "@angular-eslint/template-parser": "21.1.0", "@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", "typescript": "*", "typescript-eslint": "^8.0.0" @@ -7202,16 +4890,6 @@ "@angular/core": ">=20.0.0" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ansi-escapes": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", @@ -7228,19 +4906,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -7324,17 +4989,10 @@ "node": ">= 0.4" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT" - }, "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", "dev": true, "funding": [ { @@ -7352,10 +5010,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -7379,75 +5036,6 @@ "node": ">= 0.4" } }, - "node_modules/babel-loader": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", - "integrity": "sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5.61.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -7466,22 +5054,15 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", - "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz", + "integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==", "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT" - }, "node_modules/beasties": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.5.tgz", @@ -7502,16 +5083,6 @@ "node": ">=14.0.0" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7549,17 +5120,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -7590,9 +5150,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -7610,11 +5170,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "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.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -7630,22 +5190,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -7657,92 +5201,83 @@ } }, "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^4.0.0", + "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "minimatch": "^10.1.1", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, - "node_modules/cacache/node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "node_modules/cacache/node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/call-bind-apply-helpers": { @@ -7796,9 +5331,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "dev": true, "funding": [ { @@ -7887,23 +5422,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" + "node": ">=18" } }, "node_modules/cli-cursor": { @@ -7923,30 +5448,47 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", + "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8008,34 +5550,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8072,86 +5586,6 @@ "node": "^12.20.0 || >=14" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8175,16 +5609,6 @@ "node": ">= 0.10.0" } }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -8311,6 +5735,8 @@ "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "is-what": "^3.14.1" }, @@ -8318,30 +5744,6 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/copy-webpack-plugin": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", - "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, "node_modules/core-js": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", @@ -8353,20 +5755,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-compat": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", - "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -8387,33 +5775,6 @@ "node": ">= 0.10" } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8429,42 +5790,6 @@ "node": ">= 8" } }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/css-select": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", @@ -8562,49 +5887,6 @@ "dev": true, "license": "MIT" }, - "node_modules/default-browser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", - "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8637,13 +5919,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT" - }, "node_modules/di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -8663,19 +5938,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -8763,13 +6025,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8778,9 +6033,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -8791,16 +6046,6 @@ "dev": true, "license": "MIT" }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -8954,20 +6199,6 @@ } } }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/ent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", @@ -9051,6 +6282,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "prr": "~1.0.1" }, @@ -9058,16 +6290,6 @@ "errno": "cli.js" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -9088,13 +6310,6 @@ "node": ">= 0.4" } }, - "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==", - "dev": true, - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -9149,9 +6364,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", - "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.26.0.tgz", + "integrity": "sha512-3Hq7jri+tRrVWha+ZeIVhl4qJRha/XjRNSopvTsOaCvfPHrflTYTcUFcEjMKdxofsXXsdc4zjg5NOTnL4Gl57Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9162,45 +6377,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.9", - "@esbuild/android-arm": "0.25.9", - "@esbuild/android-arm64": "0.25.9", - "@esbuild/android-x64": "0.25.9", - "@esbuild/darwin-arm64": "0.25.9", - "@esbuild/darwin-x64": "0.25.9", - "@esbuild/freebsd-arm64": "0.25.9", - "@esbuild/freebsd-x64": "0.25.9", - "@esbuild/linux-arm": "0.25.9", - "@esbuild/linux-arm64": "0.25.9", - "@esbuild/linux-ia32": "0.25.9", - "@esbuild/linux-loong64": "0.25.9", - "@esbuild/linux-mips64el": "0.25.9", - "@esbuild/linux-ppc64": "0.25.9", - "@esbuild/linux-riscv64": "0.25.9", - "@esbuild/linux-s390x": "0.25.9", - "@esbuild/linux-x64": "0.25.9", - "@esbuild/netbsd-arm64": "0.25.9", - "@esbuild/netbsd-x64": "0.25.9", - "@esbuild/openbsd-arm64": "0.25.9", - "@esbuild/openbsd-x64": "0.25.9", - "@esbuild/openharmony-arm64": "0.25.9", - "@esbuild/sunos-x64": "0.25.9", - "@esbuild/win32-arm64": "0.25.9", - "@esbuild/win32-ia32": "0.25.9", - "@esbuild/win32-x64": "0.25.9" - } - }, - "node_modules/esbuild-wasm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.9.tgz", - "integrity": "sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==", - "dev": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.26.0", + "@esbuild/android-arm": "0.26.0", + "@esbuild/android-arm64": "0.26.0", + "@esbuild/android-x64": "0.26.0", + "@esbuild/darwin-arm64": "0.26.0", + "@esbuild/darwin-x64": "0.26.0", + "@esbuild/freebsd-arm64": "0.26.0", + "@esbuild/freebsd-x64": "0.26.0", + "@esbuild/linux-arm": "0.26.0", + "@esbuild/linux-arm64": "0.26.0", + "@esbuild/linux-ia32": "0.26.0", + "@esbuild/linux-loong64": "0.26.0", + "@esbuild/linux-mips64el": "0.26.0", + "@esbuild/linux-ppc64": "0.26.0", + "@esbuild/linux-riscv64": "0.26.0", + "@esbuild/linux-s390x": "0.26.0", + "@esbuild/linux-x64": "0.26.0", + "@esbuild/netbsd-arm64": "0.26.0", + "@esbuild/netbsd-x64": "0.26.0", + "@esbuild/openbsd-arm64": "0.26.0", + "@esbuild/openbsd-x64": "0.26.0", + "@esbuild/openharmony-arm64": "0.26.0", + "@esbuild/sunos-x64": "0.26.0", + "@esbuild/win32-arm64": "0.26.0", + "@esbuild/win32-ia32": "0.26.0", + "@esbuild/win32-x64": "0.26.0" } }, "node_modules/escalade": { @@ -9234,9 +6436,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -9246,7 +6448,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -9530,16 +6732,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -9571,19 +6763,20 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -9726,19 +6919,6 @@ "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -9782,9 +6962,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -9796,7 +6976,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/find-up": { @@ -9816,16 +7000,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -9868,23 +7042,6 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10083,23 +7240,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -10164,20 +7304,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -10243,28 +7369,15 @@ } }, "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10312,13 +7425,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT" - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -10340,13 +7446,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT" - }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -10376,24 +7475,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-middleware": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", - "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.15", - "debug": "^4.3.6", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.3", - "is-plain-object": "^5.0.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -10408,20 +7489,10 @@ "node": ">= 14" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - } - }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz", + "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==", "dev": true, "license": "MIT", "dependencies": { @@ -10435,19 +7506,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -10494,6 +7552,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "bin": { "image-size": "bin/image-size.js" }, @@ -10589,13 +7648,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -10623,22 +7675,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -10649,13 +7685,16 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10673,25 +7712,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -10705,19 +7725,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10727,29 +7734,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -10794,23 +7778,9 @@ "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", "dev": true, - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "optional": true, + "peer": true }, "node_modules/isarray": { "version": "1.0.0", @@ -10838,16 +7808,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -10929,60 +7889,13 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jasmine-core": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.12.1.tgz", - "integrity": "sha512-P/UbRZ0LKwXe7wEpwDheuhunPwITn4oPALhrJEQJo6756EwNGnsK/TSQrWojBB4cQDQ+VaxWYws9tFNDuiMh2Q==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.13.0.tgz", + "integrity": "sha512-vsYjfh7lyqvZX5QgqKc4YH8phs7g96Z8bsdIFNEU3VqXhlHaq+vov/Fgn/sr6MiUczdZkyXRC3TX369Ll4Nzbw==", "dev": true, "license": "MIT" }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -10994,6 +7907,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11035,13 +7958,13 @@ "license": "MIT" }, "node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/json-schema-traverse": { @@ -11285,16 +8208,6 @@ "dev": true, "license": "MIT" }, - "node_modules/karma-source-map-support": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", - "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map-support": "^0.5.5" - } - }, "node_modules/karma/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -11678,33 +8591,13 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, "node_modules/less": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/less/-/less-4.4.2.tgz", "integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==", "dev": true, "license": "Apache-2.0", + "optional": true, "peer": true, "dependencies": { "copy-anything": "^2.0.1", @@ -11727,33 +8620,6 @@ "source-map": "~0.6.0" } }, - "node_modules/less-loader": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.3.0.tgz", - "integrity": "sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -11835,24 +8701,6 @@ "node": ">= 0.8.0" } }, - "node_modules/license-webpack-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", - "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", - "dev": true, - "license": "ISC", - "dependencies": { - "webpack-sources": "^3.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } - } - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -11881,13 +8729,13 @@ "license": "MIT" }, "node_modules/listr2": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", - "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -11937,9 +8785,9 @@ } }, "node_modules/lmdb": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.2.tgz", - "integrity": "sha512-nwVGUfTBUwJKXd6lRV8pFNfnrCC1+l49ESJRM19t/tFb/97QfJEixe5DYRvug5JO7DSFKoKaVy7oGMt5rVqZvg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.4.3.tgz", + "integrity": "sha512-GWV1kVi6uhrXWqe+3NXWO73OYe8fto6q8JMo0HOpk1vf8nEyFWgo4CSNJpIFzsOxOrysVUlcO48qRbQfmKd1gA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -11955,37 +8803,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.4.2", - "@lmdb/lmdb-darwin-x64": "3.4.2", - "@lmdb/lmdb-linux-arm": "3.4.2", - "@lmdb/lmdb-linux-arm64": "3.4.2", - "@lmdb/lmdb-linux-x64": "3.4.2", - "@lmdb/lmdb-win32-arm64": "3.4.2", - "@lmdb/lmdb-win32-x64": "3.4.2" - } - }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" + "@lmdb/lmdb-darwin-arm64": "3.4.3", + "@lmdb/lmdb-darwin-x64": "3.4.3", + "@lmdb/lmdb-linux-arm": "3.4.3", + "@lmdb/lmdb-linux-arm64": "3.4.3", + "@lmdb/lmdb-linux-x64": "3.4.3", + "@lmdb/lmdb-win32-arm64": "3.4.3", + "@lmdb/lmdb-win32-x64": "3.4.3" } }, "node_modules/localforage": { @@ -12040,13 +8864,6 @@ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -12062,14 +8879,14 @@ "license": "MIT" }, "node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { "node": ">=18" @@ -12078,32 +8895,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols/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/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -12137,39 +8928,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -12216,13 +8974,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-dir": { @@ -12242,26 +9000,49 @@ } }, "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", - "proc-log": "^5.0.0", + "proc-log": "^6.0.0", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "ssri": "^13.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/marks-pane": { @@ -12290,25 +9071,6 @@ "node": ">= 0.8" } }, - "node_modules/memfs": { - "version": "4.51.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.51.0.tgz", - "integrity": "sha512-4zngfkVM/GpIhC8YazOsM6E8hoB33NP0BCESPOA6z7qaL6umPJNqkO8CNYaLV2FB2MV6H1O3x2luHHOSqppv+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -12322,13 +9084,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -12338,16 +9093,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -12426,34 +9171,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -12504,9 +9221,9 @@ } }, "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.0.tgz", + "integrity": "sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==", "dev": true, "license": "MIT", "dependencies": { @@ -12515,7 +9232,7 @@ "minizlib": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" }, "optionalDependencies": { "encoding": "^0.1.13" @@ -12664,9 +9381,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.7.tgz", + "integrity": "sha512-LXTGyT6FlVaL090rEEz4ZwfifKEj+tF4FOoaC6cEbHxvIDu8qc3QPXmLHKQO+XSOmvhbXZOCYPhFG4VCBZyJfQ==", "dev": true, "license": "MIT", "optional": true, @@ -12697,20 +9414,6 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -12764,6 +9467,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" @@ -12782,6 +9486,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -12799,13 +9504,6 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -12845,9 +9543,9 @@ } }, "node_modules/ngx-extended-pdf-viewer": { - "version": "25.6.1", - "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-25.6.1.tgz", - "integrity": "sha512-NZ2msuI9tRi9yVvTEBqsdleYfsiEa5FNrZdhNvM0NXlh9VoZNH+MRiGo5SQPyoVCRi3iQX5PhBcboQBGPDlxMg==", + "version": "25.6.4", + "resolved": "https://registry.npmjs.org/ngx-extended-pdf-viewer/-/ngx-extended-pdf-viewer-25.6.4.tgz", + "integrity": "sha512-eYIiWzatcupB7HKDtcOOZN7gcLFjqAkeIAlZOMIO6XyUJnTe+PUZLZGit/19mtO/8fAaH41lMyyh8MAcU8NAhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" @@ -12858,16 +9556,16 @@ } }, "node_modules/ngx-infinite-scroll": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-20.0.0.tgz", - "integrity": "sha512-Mh7lg85jeDPzZirxPHjyMagCcC3vbk+yPO5uoXHNkmGer8MrO6vOydOX306qY4QYDyMJ+ngIQgpl5HJXfc590A==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-21.0.0.tgz", + "integrity": "sha512-bm1hCB7aoO9zyiNZBBOLHx9t+cwk/tLiFG5eQB8pWiNup6I2eQiHoT5B2gqlJjj4GfuER5phy5AIWl/B7YiwSQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=20.0.0 <21.0.0", - "@angular/core": ">=20.0.0 <21.0.0" + "@angular/common": ">=21.0.0 <22.0.0", + "@angular/core": ">=21.0.0 <22.0.0" } }, "node_modules/node-addon-api": { @@ -12878,39 +9576,29 @@ "license": "MIT", "optional": true }, - "node_modules/node-forge": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", - "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.1.0.tgz", + "integrity": "sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.2", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp-build-optional-packages": { @@ -12929,16 +9617,6 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -12949,27 +9627,20 @@ "node": ">=16" } }, - "node_modules/node-gyp/node_modules/tar": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", - "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "node_modules/node-gyp/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, + "license": "ISC", "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", "dev": true, "license": "ISC", "dependencies": { @@ -12979,17 +9650,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-releases": { @@ -13000,19 +9661,19 @@ "license": "MIT" }, "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^3.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/normalize-path": { @@ -13024,16 +9685,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-bundled": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", @@ -13048,16 +9699,16 @@ } }, "node_modules/npm-install-checks": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", - "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-normalize-package-bin": { @@ -13071,9 +9722,9 @@ } }, "node_modules/npm-package-arg": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.0.tgz", - "integrity": "sha512-+t2etZAGcB7TbbLHfDwooV9ppB2LhhcT6A+L9cahsf9mEUAoQ6CktLEVvEnpD0N5CkX7zJqnPGaFtoQDy9EkHQ==", + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.1.tgz", + "integrity": "sha512-6zqls5xFvJbgFjB1B2U6yITtyGBjDBORB7suI4zA4T/sZ1OmkMFlaQSNB/4K0LtXNA1t4OprAFxPisadK5O2ag==", "dev": true, "license": "ISC", "dependencies": { @@ -13111,111 +9762,59 @@ } }, "node_modules/npm-pick-manifest": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", - "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", "dev": true, "license": "ISC", "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-pick-manifest/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-pick-manifest/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-pick-manifest/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm-registry-fetch": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", - "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/redact": "^3.0.0", + "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", + "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-registry-fetch/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm-registry-fetch/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/npm-registry-fetch/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nth-check": { @@ -13262,13 +9861,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -13282,16 +9874,6 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -13318,25 +9900,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -13356,24 +9919,24 @@ } }, "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", + "chalk": "^5.6.2", "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", + "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13392,6 +9955,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/ora/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ordered-binary": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz", @@ -13445,65 +10025,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pacote": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.0.tgz", - "integrity": "sha512-lcqexq73AMv6QNLo7SOpz0JJoaGdS3rBFgF122NZVl1bApo2mfu+XzUBU/X/XsiJu+iUmKpekRayqQYAs+PhkA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.3.tgz", + "integrity": "sha512-itdFlanxO0nmQv4ORsvA9K1wv40IPfB9OmWqfaJWvoJ30VKyHsqNgDVeG+TVhI7Gk7XW8slUy7cA9r6dF5qohw==", "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", + "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", + "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^10.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", + "sigstore": "^4.0.0", "ssri": "^12.0.0", - "tar": "^6.1.11" + "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" @@ -13512,42 +10057,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/pacote/node_modules/hosted-git-info": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", - "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/pacote/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/pacote/node_modules/npm-package-arg": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", - "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -13573,38 +10082,14 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.10" } @@ -13721,28 +10206,31 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/path-to-regexp": { "version": "8.3.0", @@ -13932,48 +10420,6 @@ } } }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/postcss-loader/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -13981,97 +10427,6 @@ "dev": true, "license": "MIT" }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", @@ -14147,24 +10502,24 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-20.4.0.tgz", - "integrity": "sha512-vXUD1G4/uet4rDkPW8xx7yZWj7RmsmexEJ3+GhpQgsNaLtPFsTCVfQq8v4FQ4tIs7shoD0hz76d3jtjGWZ49QQ==", + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.0.2.tgz", + "integrity": "sha512-suf+xK3V3z2aG9OmQMAriqhMt79JM3jBSoRDu4XQKCiTiWDRnOvoVdCZCjByT32sE5tYqhrLBsCH0lT7RLcosg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { + "@primeuix/motion": "^0.0.10", "@primeuix/styled": "^0.7.4", - "@primeuix/styles": "^1.2.5", - "@primeuix/utils": "^0.6.2", + "@primeuix/styles": "^2.0.2", + "@primeuix/utils": "^0.6.3", "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^20.0.4", - "@angular/cdk": "^20.0.3", - "@angular/common": "^20.0.4", - "@angular/core": "^20.0.4", - "@angular/forms": "^20.0.4", - "@angular/platform-browser": "^20.0.4", - "@angular/router": "^20.0.4", + "@angular/cdk": "^21.0.0", + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/forms": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/router": "^21.0.0", "rxjs": "^6.0.0 || ^7.8.1" } }, @@ -14218,7 +10573,8 @@ "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/punycode": { "version": "1.4.1", @@ -14308,16 +10664,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14389,71 +10735,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regex-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", - "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -14511,48 +10792,6 @@ "node": ">=4" } }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -14614,10 +10853,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolldown": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.47.tgz", + "integrity": "sha512-Mid74GckX1OeFAOYz9KuXeWYhq3xkXbMziYIC+ULVdUzPTG9y70OBSBQDQn9hQP8u/AfhuYw1R0BSg15nBI4Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.96.0", + "@rolldown/pluginutils": "1.0.0-beta.47" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.47", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.47", + "@rolldown/binding-darwin-x64": "1.0.0-beta.47", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.47", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.47", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.47", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.47", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.47", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.47", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.47", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.47", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.47", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.47", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.47" + } + }, "node_modules/rollup": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", - "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14631,28 +10903,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.3", - "@rollup/rollup-android-arm64": "4.52.3", - "@rollup/rollup-darwin-arm64": "4.52.3", - "@rollup/rollup-darwin-x64": "4.52.3", - "@rollup/rollup-freebsd-arm64": "4.52.3", - "@rollup/rollup-freebsd-x64": "4.52.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", - "@rollup/rollup-linux-arm-musleabihf": "4.52.3", - "@rollup/rollup-linux-arm64-gnu": "4.52.3", - "@rollup/rollup-linux-arm64-musl": "4.52.3", - "@rollup/rollup-linux-loong64-gnu": "4.52.3", - "@rollup/rollup-linux-ppc64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-musl": "4.52.3", - "@rollup/rollup-linux-s390x-gnu": "4.52.3", - "@rollup/rollup-linux-x64-gnu": "4.52.3", - "@rollup/rollup-linux-x64-musl": "4.52.3", - "@rollup/rollup-openharmony-arm64": "4.52.3", - "@rollup/rollup-win32-arm64-msvc": "4.52.3", - "@rollup/rollup-win32-ia32-msvc": "4.52.3", - "@rollup/rollup-win32-x64-gnu": "4.52.3", - "@rollup/rollup-win32-x64-msvc": "4.52.3", + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" } }, @@ -14673,19 +10945,6 @@ "node": ">= 18" } }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14755,8 +11014,6 @@ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -14772,113 +11029,14 @@ "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass-loader": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", - "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/sax": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", "dev": true, "license": "BlueOak-1.0.0", - "optional": true - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } + "optional": true, + "peer": true }, "node_modules/semver": { "version": "7.7.3", @@ -14894,175 +11052,36 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/serve-index/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -15073,6 +11092,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setimmediate": { @@ -15088,19 +11111,6 @@ "dev": true, "license": "ISC" }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15124,19 +11134,6 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/showdown": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz", @@ -15243,35 +11240,35 @@ } }, "node_modules/sigstore": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", - "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.0.0.tgz", + "integrity": "sha512-Gw/FgHtrLM9WP8P5lLcSGh9OQcrTruWCELAiS48ik1QbL0cH+dfjomiRTUE9zzz+D1N6rOLkwXUvVmXZAsNE0Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "@sigstore/sign": "^3.1.0", - "@sigstore/tuf": "^3.1.0", - "@sigstore/verify": "^2.1.0" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.0.0", + "@sigstore/tuf": "^4.0.0", + "@sigstore/verify": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "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.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" @@ -15468,28 +11465,6 @@ "node": ">= 0.6" } }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -15539,40 +11514,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-loader/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -15630,53 +11571,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", @@ -15755,62 +11649,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -15827,30 +11665,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -16048,120 +11862,32 @@ "node": ">=8.10.0" } }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/terser": { "version": "5.44.0", @@ -16169,6 +11895,8 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -16182,47 +11910,14 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/thenify": { "version": "3.3.1", @@ -16245,30 +11940,6 @@ "node": ">=0.8" } }, - "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -16317,33 +11988,6 @@ "node": ">=0.6" } }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -16370,18 +12014,18 @@ "license": "0BSD" }, "node_modules/tuf-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", - "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.0.0.tgz", + "integrity": "sha512-Lq7ieeGvXDXwpoSmOSgLWVdsGGV9J4a77oDTAPe/Ltrqnnm/ETaRlBAQTH5JatEh8KXuE6sddf9qAv1Q2282Hg==", "dev": true, "license": "MIT", "dependencies": { - "@tufjs/models": "3.0.1", + "@tufjs/models": "4.0.0", "debug": "^4.4.1", - "make-fetch-happen": "^14.0.3" + "make-fetch-happen": "^15.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/type": { @@ -16418,13 +12062,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-assert": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", - "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true, - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -16440,16 +12077,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.0.tgz", - "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.48.0", - "@typescript-eslint/parser": "8.48.0", - "@typescript-eslint/typescript-estree": "8.48.0", - "@typescript-eslint/utils": "8.48.0" + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16490,6 +12127,16 @@ "node": "*" } }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -16497,74 +12144,30 @@ "dev": true, "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "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": "^5.0.0" + "unique-slug": "^6.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "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": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/universalify": { @@ -16588,9 +12191,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -16700,9 +12303,9 @@ } }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16774,6 +12377,490 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -16798,16 +12885,6 @@ "node": ">=10.13.0" } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", @@ -16816,790 +12893,6 @@ "license": "MIT", "optional": true }, - "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", - "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/express-serve-static-core": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/webpack-dev-server/node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-dev-server/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-server/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/webpack-dev-server/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/webpack-dev-server/node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/webpack-dev-server/node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/webpack-dev-server/node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/webpack-dev-server/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-subresource-integrity": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", - "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "typed-assert": "^1.0.8" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", - "webpack": "^5.12.0" - }, - "peerDependenciesMeta": { - "html-webpack-plugin": { - "optional": true - } - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -17616,13 +12909,6 @@ "node": ">= 8" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17648,80 +12934,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -17805,22 +13017,6 @@ } } }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -17879,6 +13075,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", @@ -17893,9 +13102,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", "funding": { diff --git a/booklore-ui/package.json b/booklore-ui/package.json index 420a6810..5aa31fce 100644 --- a/booklore-ui/package.json +++ b/booklore-ui/package.json @@ -12,17 +12,17 @@ }, "private": true, "dependencies": { - "@angular/animations": "^20.3.5", - "@angular/cdk": "^20.2.9", - "@angular/common": "^20.3.5", - "@angular/compiler": "^20.3.5", - "@angular/core": "^20.3.5", - "@angular/forms": "^20.3.5", - "@angular/platform-browser": "^20.3.5", - "@angular/platform-browser-dynamic": "^20.3.5", - "@angular/router": "^20.3.5", + "@angular/animations": "^21.0.5", + "@angular/cdk": "^21.0.3", + "@angular/common": "^21.0.5", + "@angular/compiler": "^21.0.5", + "@angular/core": "^21.0.5", + "@angular/forms": "^21.0.5", + "@angular/platform-browser": "^21.0.5", + "@angular/platform-browser-dynamic": "^21.0.5", + "@angular/router": "^21.0.5", "@iharbeck/ngx-virtual-scroller": "^19.0.1", - "@primeng/themes": "^20.4.0", + "@primeng/themes": "^21.0.2", "@stomp/rx-stomp": "^2.3.0", "@stomp/stompjs": "^7.2.1", "@tweenjs/tween.js": "^25.0.0", @@ -34,10 +34,10 @@ "jwt-decode": "^4.0.0", "ng-lazyload-image": "^9.1.3", "ng2-charts": "^8.0.0", - "ngx-extended-pdf-viewer": "^25.6.1", - "ngx-infinite-scroll": "^20.0.0", + "ngx-extended-pdf-viewer": "^25.6.4", + "ngx-infinite-scroll": "^21.0.0", "primeicons": "^7.0.0", - "primeng": "^20.4.0", + "primeng": "^21.0.2", "quill": "^2.0.3", "rxjs": "^7.8.2", "showdown": "^2.1.0", @@ -47,16 +47,16 @@ "zone.js": "^0.16.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^20.3.5", - "@angular/cli": "^20.3.5", - "@angular/compiler-cli": "^20.3.5", + "@angular/build": "^21.0.3", + "@angular/cli": "^21.0.3", + "@angular/compiler-cli": "^21.0.5", "@tailwindcss/typography": "^0.5.19", "@types/jasmine": "^5.1.13", "@types/showdown": "^2.0.6", - "angular-eslint": "^20.3.5", - "autoprefixer": "^10.4.22", - "eslint": "^9.39.1", - "jasmine-core": "^5.12.1", + "angular-eslint": "^21.1.0", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.2", + "jasmine-core": "^5.13.0", "karma": "^6.4.4", "karma-chrome-launcher": "^3.2.0", "karma-coverage": "^2.2.1", @@ -64,6 +64,6 @@ "karma-jasmine-html-reporter": "^2.1.0", "tailwindcss": "^3.4.17", "typescript": "~5.9.3", - "typescript-eslint": "^8.48.0" + "typescript-eslint": "^8.50.0" } -} +} \ No newline at end of file diff --git a/booklore-ui/src/app/core/security/auth-interceptor.service.ts b/booklore-ui/src/app/core/security/auth-interceptor.service.ts index df6f7190..d1e237d8 100644 --- a/booklore-ui/src/app/core/security/auth-interceptor.service.ts +++ b/booklore-ui/src/app/core/security/auth-interceptor.service.ts @@ -73,7 +73,6 @@ function handle401Error(authService: AuthService, request: HttpRequest, nex } function forceLogout(authService: AuthService, router: Router, message?: string): void { - authService.logout(); - router.navigate(['/login']); if (message) console.warn(message); + authService.logout(); } diff --git a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts index 7d3180d7..17463f66 100644 --- a/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts +++ b/booklore-ui/src/app/features/book/components/additional-file-uploader/additional-file-uploader.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { CommonModule } from '@angular/common'; + import { FormsModule } from '@angular/forms'; import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog'; import { Select } from 'primeng/select'; @@ -29,14 +29,13 @@ interface UploadingFile { selector: 'app-additional-file-uploader', standalone: true, imports: [ - CommonModule, FormsModule, Select, Button, FileUpload, Badge, Tooltip - ], +], templateUrl: './additional-file-uploader.component.html', styleUrls: ['./additional-file-uploader.component.scss'] }) diff --git a/booklore-ui/src/app/features/book/components/book-browser/BookDialogHelperService.ts b/booklore-ui/src/app/features/book/components/book-browser/BookDialogHelperService.ts index ad8700cc..07b84b3c 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/BookDialogHelperService.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/BookDialogHelperService.ts @@ -1,5 +1,6 @@ import {inject, Injectable} from '@angular/core'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {DynamicDialogRef} from 'primeng/dynamicdialog'; +import {DialogLauncherService} from '../../../../shared/services/dialog-launcher.service'; import {ShelfAssignerComponent} from '../shelf-assigner/shelf-assigner.component'; import {LockUnlockMetadataDialogComponent} from './lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component'; import {MetadataRefreshType} from '../../../metadata/model/request/metadata-refresh-type.enum'; @@ -8,50 +9,61 @@ import {MultiBookMetadataEditorComponent} from '../../../metadata/component/mult import {MultiBookMetadataFetchComponent} from '../../../metadata/component/multi-book-metadata-fetch/multi-book-metadata-fetch-component'; import {FileMoverComponent} from '../../../../shared/components/file-mover/file-mover-component'; import {ShelfCreatorComponent} from '../shelf-creator/shelf-creator.component'; +import {BookSenderComponent} from '../book-sender/book-sender.component'; +import {MetadataFetchOptionsComponent} from '../../../metadata/component/metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component'; +import {BookMetadataCenterComponent} from '../../../metadata/component/book-metadata-center/book-metadata-center.component'; +import {CoverSearchComponent} from '../../../metadata/component/cover-search/cover-search.component'; +import {Book} from '../../model/book.model'; +import {AdditionalFileUploaderComponent} from '../additional-file-uploader/additional-file-uploader.component'; @Injectable({providedIn: 'root'}) export class BookDialogHelperService { - private dialogService = inject(DialogService); + private dialogLauncherService = inject(DialogLauncherService); - openShelfAssigner(bookIds: Set): DynamicDialogRef | null { - return this.dialogService.open(ShelfAssignerComponent, { - showHeader: false, - modal: true, - closable: true, - contentStyle: {overflow: 'hidden'}, - styleClass: 'dynamic-dialog-minimal', - baseZIndex: 10, + private openDialog(component: any, options: {}): DynamicDialogRef | null { + return this.dialogLauncherService.openDialog(component, options); + } + + openBookDetailsDialog(bookId: number): DynamicDialogRef | null { + return this.openDialog(BookMetadataCenterComponent, { + header: 'Book Details', + styleClass: 'book-details-dialog dialog-maximal', data: { - isMultiBooks: true, - bookIds, + bookId: bookId, }, }); } - openShelfCreator(): DynamicDialogRef { - return this.dialogService.open(ShelfCreatorComponent, { + openShelfAssignerDialog(book: Book | null, bookIds: Set | null): DynamicDialogRef | null { + const data: any = {}; + if (book !== null) { + data.isMultiBooks = false; + data.book = book; + } else if (bookIds !== null) { + data.isMultiBooks = true; + data.bookIds = bookIds; + } else { + return null; + } + return this.openDialog(ShelfAssignerComponent, { + showHeader: false, + data: data, + styleClass: 'dynamic-dialog-minimal', + }); + } + + openShelfCreatorDialog(): DynamicDialogRef { + return this.openDialog(ShelfCreatorComponent, { showHeader: false, - modal: true, - draggable: false, - dismissableMask: true, - closable: true, - contentStyle: {overflow: 'auto'}, styleClass: 'dynamic-dialog-minimal', - baseZIndex: 10, - style: { - position: 'absolute', - top: '15%', - }, })!; } openLockUnlockMetadataDialog(bookIds: Set): DynamicDialogRef | null { const count = bookIds.size; - return this.dialogService.open(LockUnlockMetadataDialogComponent, { + return this.openDialog(LockUnlockMetadataDialogComponent, { header: `Lock or Unlock Metadata for ${count} Selected Book${count > 1 ? 's' : ''}`, - modal: true, - closable: true, data: { bookIds: Array.from(bookIds), }, @@ -59,70 +71,83 @@ export class BookDialogHelperService { } openMetadataRefreshDialog(bookIds: Set): DynamicDialogRef | null { - return this.dialogService.open(MultiBookMetadataFetchComponent, { + return this.openDialog(MultiBookMetadataFetchComponent, { header: 'Metadata Refresh Options', - modal: true, - closable: true, data: { bookIds: Array.from(bookIds), metadataRefreshType: MetadataRefreshType.BOOKS, }, + styleClass: 'dialog-maximal', }); } openBulkMetadataEditDialog(bookIds: Set): DynamicDialogRef | null { - return this.dialogService.open(BulkMetadataUpdateComponent, { + return this.openDialog(BulkMetadataUpdateComponent, { header: 'Bulk Edit Metadata', - modal: true, - closable: true, - style: { - width: '90vw', - maxWidth: '1200px', - position: 'absolute' - }, data: { - bookIds: Array.from(bookIds) + bookIds: Array.from(bookIds), }, + styleClass: 'dialog-maximal' }); } openMultibookMetadataEditorDialog(bookIds: Set): DynamicDialogRef | null { - return this.dialogService.open(MultiBookMetadataEditorComponent, { - header: 'Bulk Edit Metadata', - showHeader: false, - modal: true, - closable: true, - closeOnEscape: true, - dismissableMask: true, - style: { - width: '95vw', - overflow: 'none', - }, + return this.openDialog(MultiBookMetadataEditorComponent, { + header: 'Multi-Book Metadata Editor', data: { - bookIds: Array.from(bookIds) + bookIds: Array.from(bookIds), }, + styleClass: 'dialog-full' }); } - openFileMoverDialog(selectedBooks: Set) { - const count = selectedBooks.size; - return this.dialogService.open(FileMoverComponent, { + openFileMoverDialog(bookIds: Set): DynamicDialogRef | null { + const count = bookIds.size; + return this.openDialog(FileMoverComponent, { header: `Organize Book Files (${count} book${count !== 1 ? 's' : ''})`, - showHeader: true, - maximizable: true, - modal: true, - closable: true, - closeOnEscape: false, - dismissableMask: false, - style: { - width: '95vw', - maxWidth: '97.5vw', - height: '90vh', - maxHeight: '95vh' - }, data: { - bookIds: selectedBooks + bookIds: Array.from(bookIds), }, + styleClass: 'dialog-full', + maximizable: true, + }); + } + + openCustomSendDialog(bookId: number): DynamicDialogRef | null { + return this.openDialog(BookSenderComponent, { + header: 'Send Book to Email', + data: { + bookId: bookId, + } + }); + } + + openCoverSearchDialog(bookId: number): DynamicDialogRef | null { + return this.openDialog(CoverSearchComponent, { + header: "Search Cover", + data: { + bookId: bookId, + }, + styleClass: 'dialog-maximal', + }); + } + + openMetadataFetchOptionsDialog(bookId: number): DynamicDialogRef | null { + return this.openDialog(MetadataFetchOptionsComponent, { + header: 'Metadata Refresh Options', + data: { + bookIds: [bookId], + metadataRefreshType: MetadataRefreshType.BOOKS, + } + }); + } + + openAdditionalFileUploaderDialog(book: Book): DynamicDialogRef | null { + return this.openDialog(AdditionalFileUploaderComponent, { + header: 'Upload Additional File', + data: { + book: book, + } }); } } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts index b340a379..04c4dfd4 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-browser.component.ts @@ -50,6 +50,7 @@ import {GroupRule} from '../../../magic-shelf/component/magic-shelf-component'; import {TaskHelperService} from '../../../settings/task-management/task-helper.service'; import {FilterLabelHelper} from './filter-label.helper'; import {LoadingService} from '../../../../core/services/loading.service'; +import {BookNavigationService} from '../../service/book-navigation.service'; export enum EntityType { LIBRARY = 'Library', @@ -118,10 +119,10 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { protected confirmationService = inject(ConfirmationService); protected magicShelfService = inject(MagicShelfService); protected bookRuleEvaluatorService = inject(BookRuleEvaluatorService); + protected taskHelperService = inject(TaskHelperService); private pageTitle = inject(PageTitleService); private loadingService = inject(LoadingService); - - protected taskHelperService = inject(TaskHelperService); + private bookNavigationService = inject(BookNavigationService); bookState$: Observable | undefined; entity$: Observable | undefined; @@ -256,6 +257,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { () => this.fetchMetadata(), () => this.bulkEditMetadata(), () => this.multiBookEditMetadata(), + () => this.regenerateCoversForSelected(), ); this.tieredMenuItems = this.bookMenuService.getTieredMenuItems(this.selectedBooks); @@ -584,6 +586,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { ) .subscribe(books => { this.currentBooks = books; + this.bookNavigationService.setAvailableBookIds(books.map(book => book.id)); }); } @@ -639,7 +642,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { } openShelfAssigner(): void { - this.dynamicDialogRef = this.dialogHelperService.openShelfAssigner(this.selectedBooks); + this.dynamicDialogRef = this.dialogHelperService.openShelfAssignerDialog(null, this.selectedBooks); } lockUnlockMetadata(): void { @@ -666,6 +669,38 @@ export class BookBrowserComponent implements OnInit, AfterViewInit { this.dialogHelperService.openMultibookMetadataEditorDialog(this.selectedBooks); } + regenerateCoversForSelected(): void { + if (!this.selectedBooks || this.selectedBooks.size === 0) return; + const count = this.selectedBooks.size; + this.confirmationService.confirm({ + message: `Are you sure you want to regenerate covers for ${count} book(s)?`, + header: 'Confirm Cover Regeneration', + icon: 'pi pi-image', + acceptLabel: 'Yes', + rejectLabel: 'No', + accept: () => { + this.bookService.regenerateCoversForBooks(Array.from(this.selectedBooks)).subscribe({ + next: () => { + this.messageService.add({ + severity: 'success', + summary: 'Cover Regeneration Started', + detail: `Regenerating covers for ${count} book(s). Refresh the page when complete.`, + life: 3000 + }); + }, + error: () => { + this.messageService.add({ + severity: 'error', + summary: 'Failed', + detail: 'Could not start cover regeneration.', + life: 3000 + }); + } + }); + } + }); + } + moveFiles() { this.dialogHelperService.openFileMoverDialog(this.selectedBooks); } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html index b101a052..bf9c01d7 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-card/book-card.component.html @@ -38,7 +38,7 @@ } - + @if (isCheckboxEnabled) { { - this.dialogService.open(BookSenderComponent, { - header: 'Send Book to Email', - modal: true, - closable: true, - style: { - position: 'absolute', - top: '15%', - }, - data: { - bookId: this.book.id, - } - }); + this.bookDialogHelperService.openCustomSendDialog(this.book.id); } } ] @@ -341,15 +324,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { label: 'Custom Fetch', icon: 'pi pi-sync', command: () => { - this.dialogService.open(MetadataFetchOptionsComponent, { - header: 'Metadata Refresh Options', - modal: true, - closable: true, - data: { - bookIds: [this.book!.id], - metadataRefreshType: MetadataRefreshType.BOOKS, - }, - }); + this.bookDialogHelperService.openMetadataRefreshDialog(new Set([this.book!.id])) }, } ] @@ -457,19 +432,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } private openShelfDialog(): void { - this.dialogService.open(ShelfAssignerComponent, { - header: `Update Book's Shelves`, - showHeader: false, - modal: true, - dismissableMask: true, - closable: true, - contentStyle: {overflow: 'hidden'}, - styleClass: 'dynamic-dialog-minimal', - baseZIndex: 10, - data: { - book: this.book, - }, - }); + this.bookDialogHelperService.openShelfAssignerDialog(this.book, null); } openSeriesInfo(): void { @@ -483,26 +446,17 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { } openBookInfo(book: Book): void { + const allBookIds = this.bookNavigationService.getAvailableBookIds(); + if (allBookIds.length > 0) { + this.bookNavigationService.setNavigationContext(allBookIds, book.id); + } + if (this.metadataCenterViewMode === 'route') { this.router.navigate(['/book', book.id], { queryParams: {tab: 'view'} }); } else { - this.dialogService.open(BookMetadataCenterComponent, { - width: '90%', - height: '90%', - data: {bookId: book.id}, - modal: true, - dismissableMask: true, - showHeader: true, - closable: true, - closeOnEscape: true, - draggable: false, - maximizable: false, - resizable: false, - header: 'Book Details', - styleClass: 'book-details-dialog' - }); + this.bookDialogHelperService.openBookDetailsDialog(book.id); } } @@ -669,6 +623,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { case 'epub': case 'mobi': case 'azw3': + case 'fb2': return 'pi pi-book'; case 'cbz': case 'cbr': @@ -692,6 +647,10 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy { return this.isAdmin() || (this.userPermissions?.canEditMetadata ?? false); } + canReadBook(): boolean { + return this.book?.bookType !== 'FB2'; + } + private hasDownloadPermission(): boolean { return this.isAdmin() || (this.userPermissions?.canDownload ?? false); } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts index 21b66898..b7b2dc7f 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-table/book-table.component.ts @@ -13,8 +13,6 @@ import {MessageService} from 'primeng/api'; import {Router, RouterLink} from '@angular/router'; import {filter, Subject} from 'rxjs'; import {UserService} from '../../../../settings/user-management/user.service'; -import {BookMetadataCenterComponent} from '../../../../metadata/component/book-metadata-center/book-metadata-center.component'; -import {DialogService} from 'primeng/dynamicdialog'; import {take, takeUntil} from 'rxjs/operators'; import {ReadStatusHelper} from '../../../helpers/read-status.helper'; diff --git a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts index efafb4d5..a353cec1 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/lock-unlock-metadata-dialog/lock-unlock-metadata-dialog.component.ts @@ -35,7 +35,7 @@ export class LockUnlockMetadataDialogComponent implements OnInit { 'isbn13Locked', 'isbn10Locked', 'asinLocked', 'pageCountLocked', 'thumbnailLocked', 'languageLocked', 'coverLocked', 'seriesNameLocked', 'seriesNumberLocked', 'seriesTotalLocked', 'authorsLocked', 'categoriesLocked', 'moodsLocked', 'tagsLocked', 'amazonRatingLocked', 'amazonReviewCountLocked', 'goodreadsRatingLocked', 'goodreadsReviewCountLocked', - 'hardcoverRatingLocked', 'hardcoverReviewCountLocked', 'goodreadsIdLocked', 'hardcoverIdLocked', 'googleIdLocked', 'comicvineIdLocked' + 'hardcoverRatingLocked', 'hardcoverReviewCountLocked', 'goodreadsIdLocked', 'hardcoverIdLocked', 'hardcoverBookIdLocked', 'googleIdLocked', 'comicvineIdLocked' ]; fieldLabels: Record = { @@ -66,6 +66,7 @@ export class LockUnlockMetadataDialogComponent implements OnInit { hardcoverReviewCountLocked: 'Hardcover Reviews', goodreadsIdLocked: 'Goodreads ID', hardcoverIdLocked: 'Hardcover ID', + hardcoverBookIdLocked: 'Hardcover Book ID', googleIdLocked: 'Google ID', comicvineIdLocked: 'Comicvine ID', }; diff --git a/booklore-ui/src/app/features/book/components/book-browser/metadata-restore-dialog-component/metadata-restore-dialog-component.html b/booklore-ui/src/app/features/book/components/book-browser/metadata-restore-dialog-component/metadata-restore-dialog-component.html index f9c1119c..80397c2b 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/metadata-restore-dialog-component/metadata-restore-dialog-component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/metadata-restore-dialog-component/metadata-restore-dialog-component.html @@ -80,6 +80,9 @@ @if (backupMetadata.hardcoverId) {
Hardcover ID: {{ backupMetadata.hardcoverId }}
} + @if (backupMetadata.hardcoverBookId !== null) { +
Hardcover Book ID: {{ backupMetadata.hardcoverBookId }}
+ } @if (backupMetadata.hardcoverRating !== null) {
Hardcover Rating: {{ backupMetadata.hardcoverRating }}
} diff --git a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts index e8c33622..dbb5e0ae 100644 --- a/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts +++ b/booklore-ui/src/app/features/book/components/book-reviews/book-reviews.component.ts @@ -1,5 +1,5 @@ import {Component, DestroyRef, inject, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; -import {CommonModule} from '@angular/common'; + import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {BookReview, BookReviewService} from './book-review-service'; import {ProgressSpinner} from 'primeng/progressspinner'; @@ -16,7 +16,7 @@ import {AppSettingsService} from '../../../../shared/service/app-settings.servic @Component({ selector: 'app-book-reviews', standalone: true, - imports: [CommonModule, ProgressSpinner, Rating, Tag, Button, FormsModule, Tooltip], + imports: [ProgressSpinner, Rating, Tag, Button, FormsModule, Tooltip], templateUrl: './book-reviews.component.html', styleUrl: './book-reviews.component.scss' }) diff --git a/booklore-ui/src/app/features/book/components/series-page/series-page.component.html b/booklore-ui/src/app/features/book/components/series-page/series-page.component.html index e640dbde..ae144e5c 100644 --- a/booklore-ui/src/app/features/book/components/series-page/series-page.component.html +++ b/booklore-ui/src/app/features/book/components/series-page/series-page.component.html @@ -1,77 +1,77 @@ @if (filteredBooks$ | async; as books) { -
- - - - - Series Details - - - - +
+ + + + + Series Details + + + + - @if (books[0]; as firstBook) { -
-
+ @if (books[0]; as firstBook) { +
+
- -
+ +
-
-
-

- {{ seriesTitle$ | async }} -

+
+
+

+ {{ seriesTitle$ | async }} +

+
+ +

+ @for (author of firstBook.metadata?.authors; track $index; let isLast = $last) { + + {{ author }} + + @if (!isLast) { + , + } + } +

+
-

- @for (author of firstBook.metadata?.authors; track $index; let isLast = $last) { - - {{ author }} - - @if (!isLast) { - , - } - } -

- -
- - @if (firstBook.metadata?.categories?.length) { -
-
- @for (category of firstBook.metadata?.categories; track category) { - - - - } -
-
- } -
-
-

- Publisher: - @if (firstBook.metadata?.publisher; as publisher) { - - {{publisher}} - - } @else { - - - } -

-

Years: {{ (yearsRange$ | async) || '-' }}

-

Number of books: {{ books.length || 0}}

-

Language: {{ firstBook.metadata?.language || "-"}}

-

Read Status: + @if (firstBook.metadata?.categories?.length) { +

+
+ @for (category of firstBook.metadata?.categories; track category) { + + + + } +
+
+ } +
+
+

+ Publisher: + @if (firstBook.metadata?.publisher; as publisher) { + + {{publisher}} + + } @else { + - + } +

+

Years: {{ (yearsRange$ | async) || '-' }}

+

Number of books: {{ books.length || 0}}

+

Language: {{ firstBook.metadata?.language || "-"}}

+

Read Status: @let s = seriesReadStatus$ | async; {{ getStatusLabel(s) }} @if (s === 'PARTIALLY_READ') { - ({{ seriesReadProgress$ | async }}) - } + ({{ seriesReadProgress$ | async }}) + }

@@ -82,45 +82,49 @@
+ [innerHTML]="(firstDescription$ | async) || 'No description available.'">
@let desc = firstDescription$ | async; @if ((desc?.length ?? 0) > 500) { - - + + }
-
- - -
-
No books found for this series.
+ @for (book of books; track book; let i = $index) { +
+ + +
+ } + @if (books.length === 0) { +
No books found for this series.
+ }
- } + } - - - + + +
} @else { -
- - -

- Loading series details... -

-
+
+ + +

+ Loading series details... +

+
} diff --git a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts index 543f839e..c8f8f132 100644 --- a/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts +++ b/booklore-ui/src/app/features/book/components/series-page/series-page.component.ts @@ -2,7 +2,7 @@ import { Component, inject, ViewChild } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { Button } from "primeng/button"; import { ActivatedRoute } from "@angular/router"; -import { AsyncPipe, NgClass, NgFor, NgIf, NgStyle } from "@angular/common"; +import { AsyncPipe, NgClass, NgStyle } from "@angular/common"; import { map, filter, switchMap } from "rxjs/operators"; import { Observable, combineLatest } from "rxjs"; import { Book, ReadStatus } from "../../model/book.model"; @@ -13,7 +13,7 @@ import { Tab, TabList, TabPanel, TabPanels, Tabs } from "primeng/tabs"; import { Tag } from "primeng/tag"; import { VirtualScrollerModule } from "@iharbeck/ngx-virtual-scroller"; import { ProgressSpinner } from "primeng/progressspinner"; -import { DialogService, DynamicDialogRef } from "primeng/dynamicdialog"; +import { DynamicDialogRef } from "primeng/dynamicdialog"; import { Router } from "@angular/router"; @Component({ @@ -25,8 +25,6 @@ import { Router } from "@angular/router"; AsyncPipe, Button, FormsModule, - NgIf, - NgFor, NgStyle, NgClass, BookCardComponent, @@ -37,9 +35,8 @@ import { Router } from "@angular/router"; TabPanels, TabPanel, Tag, - - VirtualScrollerModule, - ], + VirtualScrollerModule +], }) export class SeriesPageComponent { diff --git a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts index 68e7d97d..aa8b279f 100644 --- a/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts +++ b/booklore-ui/src/app/features/book/components/shelf-assigner/shelf-assigner.component.ts @@ -91,7 +91,7 @@ export class ShelfAssignerComponent implements OnInit { } createShelfDialog(): void { - const dialogRef = this.bookDialogHelper.openShelfCreator(); + const dialogRef = this.bookDialogHelper.openShelfCreatorDialog(); dialogRef.onClose.subscribe((created: boolean) => { if (created) { diff --git a/booklore-ui/src/app/features/book/model/book.model.ts b/booklore-ui/src/app/features/book/model/book.model.ts index 0c02eea4..31f9f381 100644 --- a/booklore-ui/src/app/features/book/model/book.model.ts +++ b/booklore-ui/src/app/features/book/model/book.model.ts @@ -3,7 +3,7 @@ import {CbxBackgroundColor, CbxFitMode, CbxPageSpread, CbxPageViewMode, CbxScrol import {BookReview} from '../components/book-reviews/book-review-service'; import {ZoomType} from 'ngx-extended-pdf-viewer'; -export type BookType = "PDF" | "EPUB" | "CBX"; +export type BookType = "PDF" | "EPUB" | "CBX" | "FB2"; export enum AdditionalFileType { ALTERNATIVE_FORMAT = 'ALTERNATIVE_FORMAT', @@ -88,6 +88,7 @@ export interface BookMetadata { goodreadsId?: string; comicvineId?: string; hardcoverId?: string; + hardcoverBookId?: number | null; googleId?: string; pageCount?: number | null; language?: string; @@ -122,6 +123,7 @@ export interface BookMetadata { comicvineIdLocked?: boolean; goodreadsIdLocked?: boolean; hardcoverIdLocked?: boolean; + hardcoverBookIdLocked?: boolean; googleIdLocked?: boolean; pageCountLocked?: boolean; languageLocked?: boolean; @@ -156,6 +158,7 @@ export interface MetadataClearFlags { goodreadsId?: boolean; comicvineId?: boolean; hardcoverId?: boolean; + hardcoverBookId?: boolean; googleId?: boolean; pageCount?: boolean; language?: boolean; diff --git a/booklore-ui/src/app/features/book/model/library.model.ts b/booklore-ui/src/app/features/book/model/library.model.ts index 9544a179..63eb8e88 100644 --- a/booklore-ui/src/app/features/book/model/library.model.ts +++ b/booklore-ui/src/app/features/book/model/library.model.ts @@ -1,7 +1,7 @@ import {SortOption} from './sort.model'; export type LibraryScanMode = 'FILE_AS_BOOK' | 'FOLDER_AS_BOOK'; -export type BookFileType = 'PDF' | 'EPUB' | 'CBX'; +export type BookFileType = 'PDF' | 'EPUB' | 'CBX' | 'FB2'; export interface Library { id?: number; diff --git a/booklore-ui/src/app/features/book/service/book-menu.service.ts b/booklore-ui/src/app/features/book/service/book-menu.service.ts index 23d85f89..d0f14b94 100644 --- a/booklore-ui/src/app/features/book/service/book-menu.service.ts +++ b/booklore-ui/src/app/features/book/service/book-menu.service.ts @@ -22,7 +22,8 @@ export class BookMenuService { autoFetchMetadata: () => void, fetchMetadata: () => void, bulkEditMetadata: () => void, - multiBookEditMetadata: () => void): MenuItem[] { + multiBookEditMetadata: () => void, + regenerateCovers: () => void): MenuItem[] { return [ { label: 'Auto Fetch Metadata', @@ -43,6 +44,11 @@ export class BookMenuService { label: 'Multi-Book Metadata Editor', icon: 'pi pi-clone', command: multiBookEditMetadata + }, + { + label: 'Regenerate Covers', + icon: 'pi pi-image', + command: regenerateCovers } ]; } diff --git a/booklore-ui/src/app/features/book/service/book-navigation.service.ts b/booklore-ui/src/app/features/book/service/book-navigation.service.ts new file mode 100644 index 00000000..5fd2096a --- /dev/null +++ b/booklore-ui/src/app/features/book/service/book-navigation.service.ts @@ -0,0 +1,86 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable} from 'rxjs'; + +export interface BookNavigationState { + bookIds: number[]; + currentIndex: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class BookNavigationService { + private navigationState$ = new BehaviorSubject(null); + private availableBookIds: number[] = []; + + setAvailableBookIds(bookIds: number[]): void { + this.availableBookIds = bookIds; + } + + getAvailableBookIds(): number[] { + return this.availableBookIds; + } + + setNavigationContext(bookIds: number[], currentBookId: number): void { + const currentIndex = bookIds.indexOf(currentBookId); + if (currentIndex !== -1) { + this.navigationState$.next({bookIds, currentIndex}); + } else { + this.navigationState$.next(null); + } + } + + getNavigationState(): Observable { + return this.navigationState$.asObservable(); + } + + canNavigatePrevious(): boolean { + const state = this.navigationState$.value; + return state !== null && state.currentIndex > 0; + } + + canNavigateNext(): boolean { + const state = this.navigationState$.value; + return state !== null && state.currentIndex < state.bookIds.length - 1; + } + + getPreviousBookId(): number | null { + const state = this.navigationState$.value; + if (state && state.currentIndex > 0) { + return state.bookIds[state.currentIndex - 1]; + } + return null; + } + + getNextBookId(): number | null { + const state = this.navigationState$.value; + if (state && state.currentIndex < state.bookIds.length - 1) { + return state.bookIds[state.currentIndex + 1]; + } + return null; + } + + updateCurrentBook(bookId: number): void { + const state = this.navigationState$.value; + if (state) { + const newIndex = state.bookIds.indexOf(bookId); + if (newIndex !== -1) { + this.navigationState$.next({ + ...state, + currentIndex: newIndex + }); + } + } + } + + getCurrentPosition(): { current: number; total: number } | null { + const state = this.navigationState$.value; + if (state) { + return { + current: state.currentIndex + 1, + total: state.bookIds.length + }; + } + return null; + } +} diff --git a/booklore-ui/src/app/features/book/service/book.service.ts b/booklore-ui/src/app/features/book/service/book.service.ts index bd4c4b70..d9306851 100644 --- a/booklore-ui/src/app/features/book/service/book.service.ts +++ b/booklore-ui/src/app/features/book/service/book.service.ts @@ -432,6 +432,17 @@ export class BookService { return this.http.post(`${this.url}/${bookId}/regenerate-cover`, {}); } + regenerateCoversForBooks(bookIds: number[]): Observable { + return this.http.post(`${this.url}/bulk-regenerate-covers`, { bookIds }); + } + + bulkUploadCover(bookIds: number[], file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('bookIds', bookIds.join(',')); + return this.http.post(`${this.url}/bulk-upload-cover`, formData); + } + /*------------------ All the metadata related calls go here ------------------*/ diff --git a/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts b/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts index 33e69671..d860d916 100644 --- a/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts +++ b/booklore-ui/src/app/features/book/service/library-shelf-menu.service.ts @@ -5,19 +5,13 @@ import {LibraryService} from './library.service'; import {ShelfService} from './shelf.service'; import {Library} from '../model/library.model'; import {Shelf} from '../model/shelf.model'; -import {DialogService} from 'primeng/dynamicdialog'; import {MetadataRefreshType} from '../../metadata/model/request/metadata-refresh-type.enum'; -import {LibraryCreatorComponent} from '../../library-creator/library-creator.component'; -import {ShelfEditDialogComponent} from '../components/shelf-edit-dialog/shelf-edit-dialog.component'; import {MagicShelf, MagicShelfService} from '../../magic-shelf/service/magic-shelf.service'; -import {MetadataFetchOptionsComponent} from '../../metadata/component/metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component'; -import {MagicShelfComponent} from '../../magic-shelf/component/magic-shelf-component'; -import {TaskCreateRequest, TaskType} from '../../settings/task-management/task.service'; -import {MetadataRefreshRequest} from '../../metadata/model/request/metadata-refresh-request.model'; import {TaskHelperService} from '../../settings/task-management/task-helper.service'; import {UserService} from "../../settings/user-management/user.service"; import {LoadingService} from '../../../core/services/loading.service'; import {finalize} from 'rxjs'; +import {DialogLauncherService} from '../../../shared/services/dialog-launcher.service'; @Injectable({ providedIn: 'root', @@ -30,7 +24,7 @@ export class LibraryShelfMenuService { private shelfService = inject(ShelfService); private taskHelperService = inject(TaskHelperService); private router = inject(Router); - private dialogService = inject(DialogService); + private dialogLauncherService = inject(DialogLauncherService); private magicShelfService = inject(MagicShelfService); private userService = inject(UserService); private loadingService = inject(LoadingService); @@ -44,17 +38,7 @@ export class LibraryShelfMenuService { label: 'Edit Library', icon: 'pi pi-pen-to-square', command: () => { - this.dialogService.open(LibraryCreatorComponent, { - header: 'Edit Library', - modal: true, - closable: true, - showHeader: false, - styleClass: 'dynamic-dialog-minimal', - data: { - mode: 'edit', - libraryId: entity?.id - } - }); + this.dialogLauncherService.openLibraryEditDialog(entity?.id); } }, { @@ -93,15 +77,7 @@ export class LibraryShelfMenuService { label: 'Custom Fetch Metadata', icon: 'pi pi-sync', command: () => { - this.dialogService.open(MetadataFetchOptionsComponent, { - header: 'Metadata Refresh Options', - modal: true, - closable: true, - data: { - libraryId: entity?.id, - metadataRefreshType: MetadataRefreshType.LIBRARY - } - }) + this.dialogLauncherService.openLibraryMetadataFetchDialog(entity?.id); } }, { @@ -168,16 +144,7 @@ export class LibraryShelfMenuService { label: 'Edit Shelf', icon: 'pi pi-pen-to-square', command: () => { - this.dialogService.open(ShelfEditDialogComponent, { - header: 'Edit Shelf', - modal: true, - closable: true, - showHeader: false, - styleClass: 'dynamic-dialog-minimal', - data: { - shelfId: entity?.id - }, - }) + this.dialogLauncherService.openShelfEditDialog(entity?.id); } }, { @@ -230,17 +197,7 @@ export class LibraryShelfMenuService { icon: 'pi pi-pen-to-square', disabled: disableOptions, command: () => { - this.dialogService.open(MagicShelfComponent, { - header: 'Edit Magic Shelf', - modal: true, - closable: true, - showHeader: false, - styleClass: 'dynamic-dialog-minimal', - data: { - id: entity?.id, - editMode: true, - } - }) + this.dialogLauncherService.openMagicShelfEditDialog(entity?.id); } }, { diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.html new file mode 100644 index 00000000..e30d53a5 --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.html @@ -0,0 +1,118 @@ +
+
+ + Select which fields to apply to {{ fileCount }} selected file(s). Only checked fields will be updated. +
+ + + +
+

Text Fields

+
+ @for (field of textFields; track field.name) { +
+ + + + +
+ } +
+
+ + + +
+

Number Fields

+
+ @for (field of numberFields; track field.name) { +
+ + + + +
+ } +
+
+ + + +
+
+

Array Fields

+
+ Mode: + + +
+
+

Type and press Enter to add each item.

+
+ @for (field of chipFields; track field.name) { +
+ + + + + +
+ } +
+
+ + + + +
diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.scss b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.scss new file mode 100644 index 00000000..3b1753a5 --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.scss @@ -0,0 +1,104 @@ +.bulk-edit-container { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0 1rem; + + @media (max-width: 768px) { + padding: 0; + } + + .helper-text { + font-size: 0.875rem; + color: var(--text-color-secondary); + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + padding: 0.5rem 0; + + i { + font-size: 1rem; + } + } +} + +.info-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: rgba(59, 130, 246, 0.08); + border: 1px solid var(--p-primary-color); + border-radius: 6px; + color: var(--p-text-color); + + i { + font-size: 1.25rem; + color: var(--p-primary-color); + } +} + +.fields-section { + display: flex; + flex-direction: column; + gap: 0.75rem; + + h4 { + margin: 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--p-text-secondary-color); + } +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.merge-toggle { + display: flex; + align-items: center; + gap: 0.5rem; + + .merge-label { + font-size: 0.85rem; + color: var(--p-text-secondary-color); + } +} + +.field-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.field-row { + display: grid; + grid-template-columns: auto 6rem 1fr; + gap: 0.75rem; + align-items: center; +} + +.field-label { + font-size: 0.9rem; + font-weight: 500; + color: var(--p-text-color); +} + +.field-input { + width: 100%; +} + +.field-input-small { + max-width: 120px; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding-top: 0.5rem; +} diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.ts b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.ts new file mode 100644 index 00000000..93703aac --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component.ts @@ -0,0 +1,167 @@ +import {Component, inject, OnInit, ChangeDetectorRef} from '@angular/core'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {Button} from 'primeng/button'; +import {Checkbox} from 'primeng/checkbox'; +import {InputText} from 'primeng/inputtext'; +import {AutoComplete} from 'primeng/autocomplete'; +import {Divider} from 'primeng/divider'; +import {SelectButton} from 'primeng/selectbutton'; +import {BookMetadata} from '../../../book/model/book.model'; + +export interface BulkEditResult { + fields: Partial; + enabledFields: Set; + mergeArrays: boolean; +} + +interface BulkEditField { + name: string; + label: string; + type: 'text' | 'chips' | 'number'; + controlName: string; +} + +@Component({ + selector: 'app-bookdrop-bulk-edit-dialog', + standalone: true, + imports: [ + ReactiveFormsModule, + FormsModule, + Button, + Checkbox, + InputText, + AutoComplete, + Divider, + SelectButton, + ], + templateUrl: './bookdrop-bulk-edit-dialog.component.html', + styleUrl: './bookdrop-bulk-edit-dialog.component.scss' +}) +export class BookdropBulkEditDialogComponent implements OnInit { + + private readonly dialogRef = inject(DynamicDialogRef); + private readonly config = inject(DynamicDialogConfig); + private readonly cdr = inject(ChangeDetectorRef); + + fileCount: number = 0; + mergeArrays = true; + + enabledFields = new Set(); + + bulkEditForm = new FormGroup({ + seriesName: new FormControl(''), + seriesTotal: new FormControl(null), + authors: new FormControl([]), + publisher: new FormControl(''), + language: new FormControl(''), + categories: new FormControl([]), + moods: new FormControl([]), + tags: new FormControl([]), + }); + + textFields: BulkEditField[] = [ + {name: 'seriesName', label: 'Series Name', type: 'text', controlName: 'seriesName'}, + {name: 'publisher', label: 'Publisher', type: 'text', controlName: 'publisher'}, + {name: 'language', label: 'Language', type: 'text', controlName: 'language'}, + ]; + + numberFields: BulkEditField[] = [ + {name: 'seriesTotal', label: 'Series Total', type: 'number', controlName: 'seriesTotal'}, + ]; + + chipFields: BulkEditField[] = [ + {name: 'authors', label: 'Authors', type: 'chips', controlName: 'authors'}, + {name: 'categories', label: 'Genres', type: 'chips', controlName: 'categories'}, + {name: 'moods', label: 'Moods', type: 'chips', controlName: 'moods'}, + {name: 'tags', label: 'Tags', type: 'chips', controlName: 'tags'}, + ]; + + mergeOptions = [ + {label: 'Merge', value: true}, + {label: 'Replace', value: false}, + ]; + + ngOnInit(): void { + this.fileCount = this.config.data?.fileCount ?? 0; + this.setupFormValueChangeListeners(); + } + + private setupFormValueChangeListeners(): void { + Object.keys(this.bulkEditForm.controls).forEach(fieldName => { + const control = this.bulkEditForm.get(fieldName); + control?.valueChanges.subscribe(value => { + const hasValue = Array.isArray(value) ? value.length > 0 : (value !== null && value !== '' && value !== undefined); + if (hasValue && !this.enabledFields.has(fieldName)) { + this.enabledFields.add(fieldName); + this.cdr.detectChanges(); + } + }); + }); + } + + onAutoCompleteBlur(fieldName: string, event: Event): void { + const target = event.target as HTMLInputElement; + const inputValue = target?.value?.trim(); + if (inputValue) { + const control = this.bulkEditForm.get(fieldName); + const currentValue = (control?.value as string[]) || []; + if (!currentValue.includes(inputValue)) { + control?.setValue([...currentValue, inputValue]); + } + if (target) { + target.value = ''; + } + } + + if (!this.enabledFields.has(fieldName)) { + const control = this.bulkEditForm.get(fieldName); + const value = control?.value; + if (Array.isArray(value) && value.length > 0) { + this.enabledFields.add(fieldName); + this.cdr.detectChanges(); + } + } + } + + toggleField(fieldName: string): void { + if (this.enabledFields.has(fieldName)) { + this.enabledFields.delete(fieldName); + } else { + this.enabledFields.add(fieldName); + } + } + + isFieldEnabled(fieldName: string): boolean { + return this.enabledFields.has(fieldName); + } + + get hasEnabledFields(): boolean { + return this.enabledFields.size > 0; + } + + cancel(): void { + this.dialogRef.close(null); + } + + apply(): void { + const formValue = this.bulkEditForm.value; + const fields: Partial = {}; + + this.enabledFields.forEach(fieldName => { + const value = formValue[fieldName as keyof typeof formValue]; + + if (value !== undefined && value !== null) { + (fields as Record)[fieldName] = value; + } + }); + + const result: BulkEditResult = { + fields, + enabledFields: new Set(this.enabledFields), + mergeArrays: this.mergeArrays, + }; + + this.dialogRef.close(result); + } +} diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html index dc5bb36b..f5cb409e 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component.html @@ -2,9 +2,9 @@
- +
-

Current Metadata

+

File Metadata

@@ -22,32 +22,31 @@ icon="pi pi-angle-double-left" class="mx-2" [outlined]="true" - pTooltip="Move all fields" + pTooltip="Overwrite all fields with fetched metadata" tooltipPosition="bottom" (onClick)="copyAll()" >
-

Fetched Metadata

+

Fetched

-
- +
+
- + - +
@for (field of metadataFieldsTop; track field) { -
- -
+
+ +
- +
} @for (field of metadataChips; track field) { -
- -
+
+ +
+ class="src w-full" + [ngClass]="{ + 'notneeded': !fetchedMetadata[field.fetchedKey], + }"/>
} @for (field of metadataDescription; track field) { -
- -
+
+ +
+ class="src md:!w-1/2" + disabled + [ngClass]="{ + 'notneeded': !fetchedMetadata[field.fetchedKey], + }">
} @for (field of metadataFieldsBottom; track field) { -
- -
- + +
+ @@ -189,13 +210,22 @@ [ngClass]=" { 'green-outlined-button': isValueCopied(field.controlName) && !hoveredFields[field.controlName], - 'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName] + 'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName], + 'notneeded' : !fetchedMetadata[field.fetchedKey] }" class="arrow-button" (click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)" (mouseenter)="onMouseEnter(field.controlName)" (mouseleave)="onMouseLeave(field.controlName)"/> - +
} @@ -203,40 +233,38 @@
} @else { -
+

- - Unable to fetch new metadata for this file + Unable to fetch metadata for this file

-
-
-
- +
+
+ Book Thumbnail
-
+
@for (field of metadataFieldsTop; track field) { -
- +
+
- +
+
@@ -259,17 +287,17 @@ } @for (field of metadataDescription; track field) { -
- +
+
- +
} @for (field of metadataFieldsBottom; track field) { -
- +
+
-
-
-
-

- Review Bookdrop Files - - - -

-

- These files were uploaded to the - Bookdrop Folder. - Review their fetched metadata, assign a library and subpath, and finalize where they belong in your collection. -

-
+
+
+
+

+ Review Bookdrop Files + + + +

+

+ These files were uploaded to the + Bookdrop Folder. + Review their fetched metadata, assign a library and subpath, and finalize where they belong in your collection. +

+
-
- - +
+ + +
+
+
+ @if (loading) { + +
+
+ + Loading Bookdrop files. Please wait...
- @if (loading) { + } @else { -
-
+
+ @if (saving) { +
- Loading Bookdrop files. Please wait... -
-
- - } @else { - -
- @if (saving) { -
- -
- - Organizing and moving files to their designated libraries. Please wait... - -
+
+ + Organizing and moving files to their designated libraries. Please wait... +
- } +
+ } - @if (bookdropFileUis.length !== 0) { -
-
+ @if (bookdropFileUis.length !== 0) { +
+
+
- -
- - - - - - - - -
-
- } -
- -
- @if (bookdropFileUis.length === 0) { -
- No bookdrop files to review. -
- } @else { - @for (file of bookdropFileUis; track file) { - -
-
- + + [(ngModel)]="includeCoversOnCopy"> + + + - - + + - @if (file.metadataForm.get('thumbnailUrl')?.value) { - Cover - } + + +
+ +
+ - @if (file.file.fetchedMetadata?.thumbnailUrl) { - Fetched Cover - } + + -
- {{ file.file.fileName }} -
+ + - + +
+
+ } +
+ +
+ @if (bookdropFileUis.length === 0) { +
+ No bookdrop files to review. +
+ } @else { + @for (file of bookdropFileUis; track file) { + +
+
+ + + + @if (file.metadataForm.get('thumbnailUrl')?.value) { + Cover + } @else { +
?
+ } + + @if (file.file.fetchedMetadata?.thumbnailUrl) { + Fetched Cover + } @else { +
?
+ } + +
+ {{ file.file.fileName }} +
+ +
+ @@ -174,8 +209,9 @@ [options]="libraryOptions" optionLabel="label" optionValue="value" - placeholder="Select Library" + placeholder="Library" class="library-select" + appendTo="body" [(ngModel)]="file.selectedLibraryId" (onChange)="onLibraryChange(file)"> @@ -185,7 +221,7 @@ [options]="file.availablePaths" optionLabel="name" optionValue="id" - placeholder="Select Subpath" + placeholder="Subpath" class="path-select" appendTo="body" [(ngModel)]="file.selectedPathId"> @@ -193,59 +229,76 @@ + [pTooltip]="file.showDetails + ? 'Hide metadata' + : file.file.fetchedMetadata?.title + ? 'Review and copy fetched metadata' + : 'Show file metadata (no fetched metadata)'" + tooltipPosition="left">
- - @if (file.showDetails) { - - - }
- } + + @if (file.showDetails) { + + + } +
} -
+ } +
+ } +
- + @if (!loading) { + - + }
diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.scss b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.scss index e7858dea..4d860f01 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.scss +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.scss @@ -1,7 +1,3 @@ -.container { - overflow-x: auto; -} - .main-card { display: flex; flex-direction: column; @@ -9,22 +5,24 @@ border-radius: 0.75rem; overflow: hidden; background: var(--card-background); - min-width: 60rem; border: 1px solid var(--p-content-border-color); + + @media (max-width: 768px) { + height: calc(100dvh - 4.9rem); + } } .header { - padding: 1.5rem 1rem 1rem; + padding: 1.5rem; display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; + justify-content: space-between; border-bottom: 1px solid var(--p-content-border-color); - margin-bottom: 1.5rem; - - @media (min-width: 768px) { - padding: 1.5rem 1.5rem 1rem; - flex-direction: row; - align-items: center; - justify-content: space-between; + gap: 1rem; + + @media (max-width: 768px) { + padding: 1rem; } } @@ -41,18 +39,24 @@ p { font-size: 0.875rem; color: rgb(156, 163, 175); - + strong { color: var(--primary-color); } } + + @media (max-width: 768px) { + flex-shrink: 1; + + p { + display: none; + } + } } .header-actions { - margin-top: 1rem; - - @media (min-width: 768px) { - margin-top: 0; + @media (max-width: 768px) { + display: flex; } } @@ -62,6 +66,13 @@ font-size: 0.9rem; } +.card-body { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; +} + .loading-overlay { position: absolute; inset: 0; @@ -82,7 +93,7 @@ } span { - color: rgb(209, 213, 219); + color: var(--text-color-secondary); } p-progressSpinner { @@ -92,7 +103,7 @@ } .controls-section { - padding: 0 1.5rem 1.5rem; + padding: 1rem; margin: 0; border-bottom: 1px solid var(--p-content-border-color); } @@ -135,17 +146,68 @@ justify-content: space-between; align-items: center; gap: 1rem; - padding: 0 0.25rem; + flex-wrap: wrap; + + label { + font-size: 0.875rem; + color: var(--text-color-secondary); + font-weight: 500; + } } .default-controls { display: flex; gap: 1rem; align-items: center; + justify-content: space-between; p-select { - min-width: 8rem; - max-width: 16rem; + width: 8rem; + } + + @media (max-width: 768px) { + width: 100%; + + label { + display: none; + } + + p-select { + flex-basis: 50%; + max-width: 250px; + } + } +} + +::ng-deep .p-select-overlay { + @media (max-width: 768px) { + max-width: 70vw; + + .p-select-option { + white-space: normal; + } + } +} + +.default-controls i.pi { + color: var(--p-button-outlined-info-color); +} + +p-inputgroup-addon { + background-color: transparent; + border-color: var(--p-button-outlined-info-border-color) !important; + color: var(--p-button-outlined-info-color); + padding: 0 0.5em; + + label { + display: flex; + color: var(--p-button-outlined-info-color) !important; + } +} + +p-inputgroup.disabled { + p-inputgroup-addon { + opacity: var(--p-disabled-opacity) !important; } } @@ -153,22 +215,36 @@ display: flex; gap: 1rem; align-items: center; + + @media (max-width: 640px) { + flex-wrap: wrap; + width: 100%; - span { - font-size: 0.875rem; - color: rgb(209, 213, 219); - font-weight: 500; + p-inputgroup.importmetadata { + width: 100%; + + p-button { + flex-grow: 1; + + ::ng-deep button { + width: 100%; + } + } + } + + p-button.bulkedit, p-button.extractpattern { + width: calc(50% - 0.5rem); + + ::ng-deep button { + width: 100%; + } + } } } .content-area { flex: 1; - overflow-y: auto; - padding: 1rem; - - > * + * { - margin-top: 0.5rem; - } + padding: 0 0 1px; } .empty-state { @@ -191,9 +267,9 @@ display: flex; align-items: center; gap: 1rem; - border: 1px solid var(--border-color); - border-radius: 0.75rem 0.75rem 0 0; - padding: 0.5rem 1rem; + border-bottom: 1px solid var(--border-color); + padding: 1rem; + flex-wrap: wrap; } .status-indicator { @@ -212,6 +288,17 @@ transform: scale(1.05); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } + + @media (max-width: 768px) { + display: none !important; + } +} + +div.cover-image { + display: block; + background-color: rgba(255, 255, 255, 0.1); + text-align: center; + line-height: 2rem; } .file-name { @@ -220,7 +307,11 @@ font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; + white-space: wrap; + + @media (max-width: 768px) { + max-width: 100%; + } } .metadata-status { @@ -237,34 +328,61 @@ } } +.file-library { + display: flex; + gap: 1rem; + justify-content: space-between; + align-items: center; + + @media (max-width: 768px) { + width: 100%; + } +} + .library-select, .path-select { - min-width: 8rem; - max-width: 16rem; + width: 8rem; + + @media (max-width: 768px) { + flex-basis: 50%; + max-width: 250px; + } } .details-section { - border-left: 1px solid var(--border-color); - border-right: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); - border-top: none; - border-radius: 0 0 0.625rem 0.625rem; } -.footer { - padding: 0.5rem 1rem; +.footer, .footer-mobile { + padding: 1rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; } +.footer { + @media (max-width: 768px) { + flex-wrap: wrap; + + ::ng-deep .p-button-label { + display: none; + } + + ::ng-deep .p-button { + width: var(--p-button-icon-only-width); + padding-inline-start: 0; + padding-inline-end: 0; + gap: 0; + } + } +} + .footer-left, .footer-right { display: flex; gap: 1rem; align-items: center; - min-width: 10rem; } .footer-left { @@ -272,7 +390,6 @@ } .footer-right { - min-width: 12.5rem; justify-content: flex-end; } @@ -280,6 +397,14 @@ flex-grow: 1; display: flex; justify-content: center; + + @media (max-width: 768px) { + width: 100%; + } +} + +.p-paginator { + flex-wrap: nowrap; } .spacer { diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.ts b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.ts index d1aa6c64..7b3f66e5 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.ts +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component.ts @@ -3,7 +3,7 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {filter, startWith, take, tap} from 'rxjs/operators'; import {PageTitleService} from "../../../../shared/service/page-title.service"; -import {BookdropFile, BookdropFinalizePayload, BookdropFinalizeResult, BookdropService} from '../../service/bookdrop.service'; +import {BookdropFile, BookdropFinalizePayload, BookdropFinalizeResult, BookdropService, FileExtractionResult, BulkEditRequest as BackendBulkEditRequest, BulkEditResult as BackendBulkEditResult} from '../../service/bookdrop.service'; import {LibraryService} from '../../../book/service/library.service'; import {Library} from '../../../book/model/library.model'; @@ -15,18 +15,20 @@ import {Tooltip} from 'primeng/tooltip'; import {Divider} from 'primeng/divider'; import {ConfirmationService, MessageService} from 'primeng/api'; import {Observable, Subscription} from 'rxjs'; - +import {InputGroup} from 'primeng/inputgroup'; +import {InputGroupAddonModule} from 'primeng/inputgroupaddon'; import {AppSettings} from '../../../../shared/model/app-settings.model'; import {AppSettingsService} from '../../../../shared/service/app-settings.service'; -import {DialogService} from 'primeng/dynamicdialog'; import {BookMetadata} from '../../../book/model/book.model'; import {UrlHelperService} from '../../../../shared/service/url-helper.service'; import {Checkbox} from 'primeng/checkbox'; -import {NgClass, NgStyle} from '@angular/common'; +import {NgClass} from '@angular/common'; import {Paginator} from 'primeng/paginator'; import {ActivatedRoute} from '@angular/router'; import {BookdropFileMetadataPickerComponent} from '../bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component'; -import {BookdropFinalizeResultDialogComponent} from '../bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component'; +import {BookdropBulkEditDialogComponent, BulkEditResult} from '../bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component'; +import {BookdropPatternExtractDialogComponent} from '../bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component'; +import {DialogLauncherService} from '../../../../shared/services/dialog-launcher.service'; export interface BookdropFileUI { file: BookdropFile; @@ -54,9 +56,10 @@ export interface BookdropFileUI { Tooltip, Divider, Checkbox, - NgStyle, NgClass, Paginator, + InputGroup, + InputGroupAddonModule, ], }) export class BookdropFileReviewComponent implements OnInit { @@ -64,7 +67,7 @@ export class BookdropFileReviewComponent implements OnInit { private readonly libraryService = inject(LibraryService); private readonly confirmationService = inject(ConfirmationService); private readonly destroyRef = inject(DestroyRef); - private readonly dialogService = inject(DialogService); + private readonly dialogLauncherService = inject(DialogLauncherService); private readonly appSettingsService = inject(AppSettingsService); private readonly messageService = inject(MessageService); private readonly urlHelper = inject(UrlHelperService); @@ -106,7 +109,6 @@ export class BookdropFileReviewComponent implements OnInit { .subscribe(); this.libraryService.libraryState$ - .pipe(filter(state => !!state?.loaded), take(1)) .subscribe(state => { this.libraries = state.libraries ?? []; }); @@ -209,6 +211,45 @@ export class BookdropFileReviewComponent implements OnInit { }); } + private async loadAllPagesIntoCache(): Promise { + const totalPages = Math.ceil(this.totalRecords / this.pageSize); + const pagePromises: Promise[] = []; + + for (let page = 0; page < totalPages; page++) { + const promise = new Promise((resolve, reject) => { + this.bookdropService.getPendingFiles(page, this.pageSize) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: response => { + response.content.forEach(file => { + if (!this.fileUiCache[file.id]) { + const fresh = this.createFileUI(file); + + if (this.defaultLibraryId) { + const selectedLib = this.libraries.find(l => String(l.id) === this.defaultLibraryId); + const selectedPaths = selectedLib?.paths ?? []; + fresh.selectedLibraryId = this.defaultLibraryId; + fresh.availablePaths = selectedPaths.map(p => ({id: String(p.id ?? ''), name: p.path})); + fresh.selectedPathId = this.defaultPathId ?? null; + } + + this.fileUiCache[file.id] = fresh; + } + }); + resolve(); + }, + error: err => { + console.error('Error loading page:', err); + reject(err); + } + }); + }); + pagePromises.push(promise); + } + + await Promise.all(pagePromises); + } + onLibraryChange(file: BookdropFileUI): void { const lib = this.libraries.find(l => String(l.id) === file.selectedLibraryId); file.availablePaths = lib?.paths.map(p => ({id: String(p.id ?? ''), name: p.path})) ?? []; @@ -219,44 +260,41 @@ export class BookdropFileReviewComponent implements OnInit { this.copiedFlags[fileId] = copied; } - applyDefaultsToAll(): void { + applyLibraryDefaults(): void { if (!this.defaultLibraryId || !this.libraries) return; const selectedLib = this.libraries.find(l => String(l.id) === this.defaultLibraryId); const selectedPaths = selectedLib?.paths ?? []; - Object.values(this.fileUiCache).forEach(file => { - file.selectedLibraryId = this.defaultLibraryId; - file.availablePaths = selectedPaths.map(path => ({id: String(path.id), name: path.path})); - file.selectedPathId = this.defaultPathId ?? null; + this.getSelectedFiles().map(fileUi => { + const cachedfUi = this.fileUiCache[fileUi.file.id]; + cachedfUi.selectedLibraryId = this.defaultLibraryId; + cachedfUi.availablePaths = selectedPaths.map(path => ({id: String(path.id), name: path.path})); + cachedfUi.selectedPathId = this.defaultPathId ?? null; }); } - copyAll(): void { - Object.values(this.fileUiCache).forEach(fileUi => { - const fetched = fileUi.file.fetchedMetadata; - const form = fileUi.metadataForm; + copyMetadata(): void { + const files = this.getSelectedFiles().map(fileUi => { + const cachedfUi = this.fileUiCache[fileUi.file.id]; + const fetched = cachedfUi.file.fetchedMetadata; + const form = cachedfUi.metadataForm; if (!fetched) return; for (const key of Object.keys(fetched)) { if (!this.includeCoversOnCopy && key === 'thumbnailUrl') continue; const value = fetched[key as keyof typeof fetched]; if (value != null) { form.get(key)?.setValue(value); - fileUi.copiedFields[key] = true; + cachedfUi.copiedFields[key] = true; } } - this.onMetadataCopied(fileUi.file.id, true); + this.onMetadataCopied(cachedfUi.file.id, true); }); } + resetMetadata(): void { - const selectedFiles = Object.values(this.fileUiCache).filter(file => { - if (this.selectAllAcrossPages) { - return !this.excludedFiles.has(file.file.id); - } else { - return file.selected; - } - }); + const selectedFiles = this.getSelectedFiles(); const files = selectedFiles.map(fileUi => { const original = fileUi.file.originalMetadata; @@ -281,6 +319,7 @@ export class BookdropFileReviewComponent implements OnInit { goodreadsRating: original?.goodreadsRating ?? null, goodreadsReviewCount: original?.goodreadsReviewCount ?? null, hardcoverId: original?.hardcoverId ?? null, + hardcoverBookId: original?.hardcoverBookId ?? null, hardcoverRating: original?.hardcoverRating ?? null, hardcoverReviewCount: original?.hardcoverReviewCount ?? null, googleId: original?.googleId ?? null, @@ -383,7 +422,7 @@ export class BookdropFileReviewComponent implements OnInit { icon: 'pi pi-exclamation-triangle', acceptButtonStyleClass: 'p-button-danger', accept: () => { - const payload: any = { + const payload: { selectAll: boolean; excludedIds?: number[]; selectedIds?: number[] } = { selectAll: this.selectAllAcrossPages, }; @@ -403,12 +442,7 @@ export class BookdropFileReviewComponent implements OnInit { detail: 'Selected Bookdrop files were deleted successfully.', }); - const toDelete = Object.values(this.fileUiCache).filter(file => { - return this.selectAllAcrossPages - ? !this.excludedFiles.has(file.file.id) - : file.selected; - }); - toDelete.forEach(file => delete this.fileUiCache[file.file.id]); + this.getSelectedFiles().forEach(file => delete this.fileUiCache[file.file.id]); this.selectAllAcrossPages = false; this.excludedFiles.clear(); @@ -452,13 +486,7 @@ export class BookdropFileReviewComponent implements OnInit { private finalizeImport(): void { this.saving = true; - const selectedFiles = Object.values(this.fileUiCache).filter(file => { - if (this.selectAllAcrossPages) { - return !this.excludedFiles.has(file.file.id); - } else { - return file.selected; - } - }); + const selectedFiles = this.getSelectedFiles(); const files = selectedFiles.map(fileUi => { const rawMetadata = fileUi.metadataForm.value; @@ -494,13 +522,7 @@ export class BookdropFileReviewComponent implements OnInit { detail: 'Import process finished. See details below.', }); - this.dialogService.open(BookdropFinalizeResultDialogComponent, { - header: 'Import Summary', - modal: true, - closable: true, - closeOnEscape: true, - data: {result: result}, - }); + this.dialogLauncherService.openBookdropFinalizeResultDialog(result); const finalizedIds = new Set(files.map(f => f.fileId)); Object.keys(this.fileUiCache).forEach(idStr => { @@ -546,6 +568,7 @@ export class BookdropFileReviewComponent implements OnInit { goodreadsRating: new FormControl(original?.goodreadsRating ?? ''), goodreadsReviewCount: new FormControl(original?.goodreadsReviewCount ?? ''), hardcoverId: new FormControl(original?.hardcoverId ?? ''), + hardcoverBookId: new FormControl(original?.hardcoverBookId ?? ''), hardcoverRating: new FormControl(original?.hardcoverRating ?? ''), hardcoverReviewCount: new FormControl(original?.hardcoverReviewCount ?? ''), googleId: new FormControl(original?.googleId ?? ''), @@ -591,4 +614,208 @@ export class BookdropFileReviewComponent implements OnInit { } }); } + + openBulkEditDialog(): void { + const selectedFiles = this.getSelectedFiles(); + const totalCount = this.selectAllAcrossPages + ? this.totalRecords - this.excludedFiles.size + : selectedFiles.length; + + if (totalCount === 0) { + this.messageService.add({ + severity: 'warn', + summary: 'No files selected', + detail: 'Please select files to bulk edit.', + }); + return; + } + + const dialogRef = this.dialogLauncherService.openDialog(BookdropBulkEditDialogComponent, { + header: `Bulk Edit ${totalCount} Files`, + width: '600px', + modal: true, + closable: true, + data: {fileCount: totalCount}, + }); + + dialogRef?.onClose.subscribe((result: BulkEditResult | null) => { + if (result) { + this.applyBulkMetadataViaBackend(result); + } + }); + } + + openPatternExtractDialog(): void { + const selectedFiles = this.getSelectedFiles(); + const totalCount = this.selectAllAcrossPages + ? this.totalRecords - this.excludedFiles.size + : selectedFiles.length; + + if (totalCount === 0) { + this.messageService.add({ + severity: 'warn', + summary: 'No files selected', + detail: 'Please select files to extract metadata from.', + }); + return; + } + + const sampleFiles = selectedFiles.slice(0, 5).map(f => f.file.fileName); + const selectedIds = selectedFiles.map(f => f.file.id); + + const dialogRef = this.dialogLauncherService.openDialog(BookdropPatternExtractDialogComponent, { + header: 'Extract Metadata from Filenames', + width: '700px', + modal: true, + closable: true, + data: { + sampleFiles, + fileCount: totalCount, + selectAll: this.selectAllAcrossPages, + excludedIds: Array.from(this.excludedFiles), + selectedIds, + }, + }); + + dialogRef?.onClose.subscribe((result: { results: FileExtractionResult[] } | null) => { + if (result?.results) { + this.applyExtractedMetadata(result.results); + } + }); + } + + private getSelectedFiles(): BookdropFileUI[] { + return Object.values(this.fileUiCache).filter(file => { + if (this.selectAllAcrossPages) { + return !this.excludedFiles.has(file.file.id); + } + return file.selected; + }); + } + + private async applyBulkMetadataViaBackend(result: BulkEditResult): Promise { + if (this.selectAllAcrossPages) { + try { + await this.loadAllPagesIntoCache(); + } catch (err) { + console.error('Error loading pages into cache:', err); + this.messageService.add({ + severity: 'error', + summary: 'Bulk Edit Failed', + detail: 'An error occurred while loading files into cache.', + }); + return; + } + } + + const selectedFiles = this.getSelectedFiles(); + const selectedIds = selectedFiles.map(f => f.file.id); + + this.applyBulkMetadataToUI(result, selectedFiles); + + const enabledFieldsArray = Array.from(result.enabledFields); + + const payload: BackendBulkEditRequest = { + fields: result.fields, + enabledFields: enabledFieldsArray, + mergeArrays: result.mergeArrays, + selectAll: this.selectAllAcrossPages, + excludedIds: this.selectAllAcrossPages ? Array.from(this.excludedFiles) : undefined, + selectedIds: !this.selectAllAcrossPages ? selectedIds : undefined, + }; + + this.bookdropService.bulkEditMetadata(payload).subscribe({ + next: (backendResult: BackendBulkEditResult) => { + this.messageService.add({ + severity: 'success', + summary: 'Bulk Edit Applied', + detail: `Updated metadata for ${backendResult.successfullyUpdated} of ${backendResult.totalFiles} file(s).`, + }); + }, + error: (err) => { + console.error('Error applying bulk edit:', err); + this.messageService.add({ + severity: 'error', + summary: 'Bulk Edit Failed', + detail: 'An error occurred while applying bulk edits.', + }); + }, + }); + } + + private applyBulkMetadataToUI(result: BulkEditResult, selectedFiles: BookdropFileUI[]): void { + selectedFiles.forEach(fileUi => { + result.enabledFields.forEach(fieldName => { + const value = result.fields[fieldName as keyof BookMetadata]; + if (value === undefined || value === null) { + return; + } + + if (Array.isArray(value) && value.length === 0) { + return; + } + + const control = fileUi.metadataForm.get(fieldName); + if (!control) { + return; + } + + if (result.mergeArrays && Array.isArray(value)) { + const currentValue = control.value || []; + const merged = [...new Set([...currentValue, ...value])]; + control.setValue(merged); + } else { + control.setValue(value); + } + }); + }); + } + + private async applyExtractedMetadata(results: FileExtractionResult[]): Promise { + if (this.selectAllAcrossPages) { + try { + await this.loadAllPagesIntoCache(); + } catch (err) { + console.error('Error loading pages into cache:', err); + this.messageService.add({ + severity: 'error', + summary: 'Pattern Extraction Failed', + detail: 'An error occurred while loading files into cache.', + }); + return; + } + } + + let appliedCount = 0; + + results.forEach(result => { + if (!result.success || !result.extractedMetadata) { + return; + } + + const fileUi = this.fileUiCache[result.fileId]; + if (!fileUi) { + return; + } + + Object.entries(result.extractedMetadata).forEach(([key, value]) => { + if (value === null || value === undefined) { + return; + } + + const control = fileUi.metadataForm.get(key); + if (control) { + control.setValue(value); + } + }); + + appliedCount++; + }); + + this.messageService.add({ + severity: 'success', + summary: 'Pattern Extraction Applied', + detail: `Applied extracted metadata to ${appliedCount} file(s).`, + }); + } } diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.html similarity index 100% rename from booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.html rename to booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.html diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.scss b/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.scss similarity index 100% rename from booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.scss rename to booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.scss diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.ts b/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.ts similarity index 76% rename from booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.ts rename to booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.ts index 2a05f4f5..3902d087 100644 --- a/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog-component.ts +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component.ts @@ -4,13 +4,13 @@ import {BookdropFinalizeResult} from '../../service/bookdrop.service'; import {DynamicDialogConfig, DynamicDialogRef} from "primeng/dynamicdialog"; @Component({ - selector: 'app-bookdrop-finalize-result-dialog-component', + selector: 'app-bookdrop-finalize-result-dialog', imports: [ NgClass, DatePipe ], - templateUrl: './bookdrop-finalize-result-dialog-component.html', - styleUrl: './bookdrop-finalize-result-dialog-component.scss' + templateUrl: './bookdrop-finalize-result-dialog.component.html', + styleUrl: './bookdrop-finalize-result-dialog.component.scss' }) export class BookdropFinalizeResultDialogComponent implements OnDestroy { diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.html b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.html new file mode 100644 index 00000000..900e8679 --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.html @@ -0,0 +1,123 @@ +
+
+ + + Enter a pattern to extract metadata from filenames of {{ fileCount }} selected file(s). + Use placeholders like {{ '{' }}SeriesName{{ '}' }} to capture values. + +
+ + + +
+

Pattern

+
+ + + +
+
+ +
+

Available Placeholders

+
+ @for (placeholder of availablePlaceholders; track placeholder.name) { + + + } +
+
+ +
+

Common Patterns

+
+ @for (commonPattern of commonPatterns; track commonPattern.pattern) { + + + } +
+
+ + + + @if (previewResults.length > 0) { +
+

Preview (Sample Files)

+
+ @for (preview of previewResults; track preview.fileName) { +
+
+ + {{ preview.fileName }} +
+ @if (preview.success) { +
+ @for (entry of getPreviewEntries(preview); track entry.key) { +
+ {{ entry.key }}: + {{ entry.value }} +
+ } +
+ } @else { +
{{ getErrorMessage(preview) }}
+ } +
+ } +
+
+ + + } + + +
diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.scss b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.scss new file mode 100644 index 00000000..1c7cff1d --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.scss @@ -0,0 +1,175 @@ +.pattern-extract-container { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 0 1rem; + max-height: 70vh; + + @media (max-width: 768px) { + padding: 0; + max-height: 95vh; + } +} + +.info-banner { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: rgba(59, 130, 246, 0.08); + border: 1px solid var(--p-primary-color); + border-radius: 6px; + color: var(--p-text-color); + + i { + font-size: 1.25rem; + color: var(--p-primary-color); + margin-top: 2px; + } + + code { + background: rgba(255, 255, 255, 0.1); + padding: 0.1rem 0.3rem; + border-radius: 4px; + font-family: monospace; + } +} + +.pattern-section, +.placeholders-section, +.common-patterns-section, +.preview-section { + display: flex; + flex-direction: column; + gap: 0.5rem; + + h4 { + margin: 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--p-text-secondary-color); + } +} + +.pattern-input-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.pattern-input { + flex: 1; + font-family: monospace; +} + +.placeholder-chips { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +:host ::ng-deep .placeholder-chip { + cursor: pointer; + font-family: monospace; + font-size: 0.85rem; + + &:hover { + background-color: var(--p-primary-color); + color: var(--p-primary-contrast-color); + } +} + +.common-pattern-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.preview-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 200px; + overflow-y: auto; +} + +.preview-item { + padding: 0.75rem; + border-radius: 6px; + border: 1px solid var(--p-surface-300); + + &.preview-success { + background-color: rgba(76, 175, 80, 0.1); + border-color: rgba(76, 175, 80, 0.3); + } + + &.preview-failure { + background-color: rgba(244, 67, 54, 0.1); + border-color: rgba(244, 67, 54, 0.3); + } +} + +.preview-filename { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + margin-bottom: 0.5rem; + + i { + font-size: 1rem; + } + + .pi-check-circle { + color: #4caf50; + } + + .pi-times-circle { + color: #f44336; + } +} + +.preview-extracted { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1rem; + padding-left: 1.5rem; +} + +.extracted-field { + display: flex; + gap: 0.25rem; + font-size: 0.85rem; + + .field-name { + color: var(--p-text-secondary-color); + } + + .field-value { + font-weight: 500; + color: var(--p-primary-color); + } +} + +.preview-error { + padding-left: 1.5rem; + font-size: 0.85rem; + color: var(--p-text-secondary-color); + font-style: italic; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.75rem; + padding-top: 0.5rem; +} + +.extracting-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + margin-right: auto; + color: var(--p-text-secondary-color); +} diff --git a/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.ts b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.ts new file mode 100644 index 00000000..b85299c8 --- /dev/null +++ b/booklore-ui/src/app/features/bookdrop/component/bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component.ts @@ -0,0 +1,277 @@ +import {Component, ElementRef, inject, OnInit, ViewChild} from '@angular/core'; +import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; +import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {Button} from 'primeng/button'; +import {InputText} from 'primeng/inputtext'; +import {Divider} from 'primeng/divider'; +import {Chip} from 'primeng/chip'; +import {ProgressSpinner} from 'primeng/progressspinner'; +import {BookdropService, PatternExtractResult} from '../../service/bookdrop.service'; +import {MessageService} from 'primeng/api'; +import {NgClass} from '@angular/common'; +import {Tooltip} from 'primeng/tooltip'; + +interface PatternPlaceholder { + name: string; + description: string; + example: string; +} + +interface PreviewResult { + fileName: string; + success: boolean; + preview: Record; + errorMessage?: string; +} + +@Component({ + selector: 'app-bookdrop-pattern-extract-dialog', + standalone: true, + imports: [ + ReactiveFormsModule, + Button, + InputText, + Divider, + Chip, + ProgressSpinner, + NgClass, + Tooltip, + ], + templateUrl: './bookdrop-pattern-extract-dialog.component.html', + styleUrl: './bookdrop-pattern-extract-dialog.component.scss' +}) +export class BookdropPatternExtractDialogComponent implements OnInit { + + private readonly dialogRef = inject(DynamicDialogRef); + private readonly config = inject(DynamicDialogConfig); + private readonly bookdropService = inject(BookdropService); + private readonly messageService = inject(MessageService); + + @ViewChild('patternInput', {static: false}) patternInput?: ElementRef; + + fileCount = 0; + selectAll = false; + excludedIds: number[] = []; + selectedIds: number[] = []; + + isExtracting = false; + previewResults: PreviewResult[] = []; + + patternPlaceholderText = 'e.g., {SeriesName} - Ch {SeriesNumber}'; + spinnerStyle = {width: '24px', height: '24px'}; + + patternForm = new FormGroup({ + pattern: new FormControl('', Validators.required), + }); + + availablePlaceholders: PatternPlaceholder[] = [ + {name: '*', description: 'Wildcard - skips any text (not a metadata field)', example: 'anything'}, + {name: 'SeriesName', description: 'Series or comic name', example: 'Chronicles of Earth'}, + {name: 'Title', description: 'Book title', example: 'The Lost City'}, + {name: 'Subtitle', description: 'Book subtitle', example: 'A Tale of Adventure'}, + {name: 'Authors', description: 'Author name(s)', example: 'John Smith'}, + {name: 'SeriesNumber', description: 'Book number in series', example: '25'}, + {name: 'Published', description: 'Full date with format', example: '{Published:yyyy-MM-dd}'}, + {name: 'Publisher', description: 'Publisher name', example: 'Epic Press'}, + {name: 'Language', description: 'Language code', example: 'en'}, + {name: 'SeriesTotal', description: 'Total books in series', example: '50'}, + {name: 'ISBN10', description: 'ISBN-10 identifier', example: '1234567890'}, + {name: 'ISBN13', description: 'ISBN-13 identifier', example: '1234567890123'}, + {name: 'ASIN', description: 'Amazon ASIN', example: 'B012345678'}, + ]; + + commonPatterns = [ + {label: 'Author - Title', pattern: '{Authors} - {Title}'}, + {label: 'Title - Author', pattern: '{Title} - {Authors}'}, + {label: 'Title (Year)', pattern: '{Title} ({Published:yyyy})'}, + {label: 'Author - Title (Year)', pattern: '{Authors} - {Title} ({Published:yyyy})'}, + {label: 'Series #Number', pattern: '{SeriesName} #{SeriesNumber}'}, + {label: 'Series - Chapter Number', pattern: '{SeriesName} - Chapter {SeriesNumber}'}, + {label: 'Series - Vol Number', pattern: '{SeriesName} - Vol {SeriesNumber}'}, + {label: '[Tag] Series - Chapter Number', pattern: '[*] {SeriesName} - Chapter {SeriesNumber}'}, + {label: 'Title by Author', pattern: '{Title} by {Authors}'}, + {label: 'Series vX (of Total)', pattern: '{SeriesName} v{SeriesNumber} (of {SeriesTotal})'}, + ]; + + ngOnInit(): void { + this.fileCount = this.config.data?.fileCount ?? 0; + this.selectAll = this.config.data?.selectAll ?? false; + this.excludedIds = this.config.data?.excludedIds ?? []; + this.selectedIds = this.config.data?.selectedIds ?? []; + } + + insertPlaceholder(placeholderName: string): void { + const patternControl = this.patternForm.get('pattern'); + const currentPattern = patternControl?.value ?? ''; + const inputElement = this.patternInput?.nativeElement; + + const textToInsert = placeholderName === '*' ? '*' : `{${placeholderName}}`; + + const patternToModify = placeholderName === '*' + ? currentPattern + : this.removeExistingPlaceholder(currentPattern, placeholderName); + + if (inputElement) { + const cursorPosition = this.calculateCursorPosition(inputElement, currentPattern, patternToModify); + const newPattern = this.insertTextAtCursor(patternToModify, textToInsert, cursorPosition); + + patternControl?.setValue(newPattern); + this.focusInputAfterInsertion(inputElement, cursorPosition, textToInsert.length); + } else { + patternControl?.setValue(patternToModify + textToInsert); + } + + this.previewPattern(); + } + + private removeExistingPlaceholder(pattern: string, placeholderName: string): string { + const existingPlaceholderRegex = new RegExp(`\\{${placeholderName}(?::[^}]*)?\\}`, 'g'); + return pattern.replace(existingPlaceholderRegex, ''); + } + + private calculateCursorPosition(inputElement: HTMLInputElement, originalPattern: string, modifiedPattern: string): number { + let cursorPosition = inputElement.selectionStart ?? modifiedPattern.length; + + if (originalPattern !== modifiedPattern) { + const existingPlaceholderRegex = new RegExp(`\\{\\w+(?::[^}]*)?\\}`, 'g'); + const matchBefore = originalPattern.substring(0, cursorPosition).match(existingPlaceholderRegex); + if (matchBefore) { + cursorPosition -= matchBefore.reduce((sum, match) => sum + match.length, 0); + } + cursorPosition = Math.max(0, cursorPosition); + } + + return cursorPosition; + } + + private insertTextAtCursor(pattern: string, text: string, cursorPosition: number): string { + const textBefore = pattern.substring(0, cursorPosition); + const textAfter = pattern.substring(cursorPosition); + return textBefore + text + textAfter; + } + + private focusInputAfterInsertion(inputElement: HTMLInputElement, cursorPosition: number, insertedTextLength: number): void { + setTimeout(() => { + const newCursorPosition = cursorPosition + insertedTextLength; + inputElement.setSelectionRange(newCursorPosition, newCursorPosition); + inputElement.focus(); + }, 0); + } + + applyCommonPattern(pattern: string): void { + this.patternForm.get('pattern')?.setValue(pattern); + this.previewPattern(); + } + + previewPattern(): void { + const pattern = this.patternForm.get('pattern')?.value; + if (!pattern) { + this.previewResults = []; + return; + } + + const request = { + pattern, + selectAll: this.selectAll, + excludedIds: this.excludedIds, + selectedIds: this.selectedIds, + preview: true + }; + + this.bookdropService.extractFromPattern(request).subscribe({ + next: (result) => { + this.previewResults = result.results.map(r => ({ + fileName: r.fileName, + success: r.success, + preview: r.extractedMetadata || {}, + errorMessage: r.errorMessage + })); + }, + error: () => { + this.previewResults = []; + } + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + extract(): void { + const pattern = this.patternForm.get('pattern')?.value; + if (!pattern) { + return; + } + + this.isExtracting = true; + + const payload = { + pattern, + selectAll: this.selectAll, + excludedIds: this.excludedIds, + selectedIds: this.selectedIds, + preview: false, + }; + + this.bookdropService.extractFromPattern(payload).subscribe({ + next: (result: PatternExtractResult) => { + this.isExtracting = false; + this.messageService.add({ + severity: 'success', + summary: 'Extraction Complete', + detail: `Successfully extracted metadata from ${result.successfullyExtracted} of ${result.totalFiles} files.`, + }); + this.dialogRef.close(result); + }, + error: (err) => { + this.isExtracting = false; + console.error('Pattern extraction failed:', err); + this.messageService.add({ + severity: 'error', + summary: 'Extraction Failed', + detail: 'An error occurred during pattern extraction.', + }); + }, + }); + } + + get hasValidPattern(): boolean { + const pattern: string = this.patternForm.get('pattern')?.value ?? ''; + if (!this.patternForm.valid || !pattern) { + return false; + } + const placeholderRegex = /\{[a-zA-Z0-9_]+(?::[^{}]+)?\}|\*/; + return placeholderRegex.test(pattern); + } + + getPlaceholderLabel(name: string): string { + return name === '*' ? '*' : `{${name}}`; + } + + getPlaceholderTooltip(placeholder: PatternPlaceholder): string { + return `${placeholder.description} (e.g., ${placeholder.example})`; + } + + getPreviewClass(preview: PreviewResult): Record { + return { + 'preview-success': preview.success, + 'preview-failure': !preview.success + }; + } + + getPreviewIconClass(preview: PreviewResult): string { + return preview.success ? 'pi-check-circle' : 'pi-times-circle'; + } + + getPreviewEntries(preview: PreviewResult): Array<{key: string; value: string}> { + return Object.entries(preview.preview).map(([key, value]) => ({key, value})); + } + + getErrorMessage(preview: PreviewResult): string { + return preview.errorMessage || 'Pattern did not match'; + } + + getErrorTooltip(preview: PreviewResult): string { + return preview.success ? '' : (preview.errorMessage || 'Pattern did not match filename structure'); + } +} diff --git a/booklore-ui/src/app/features/bookdrop/service/bookdrop.service.ts b/booklore-ui/src/app/features/bookdrop/service/bookdrop.service.ts index 69a2b7b4..685b4af9 100644 --- a/booklore-ui/src/app/features/bookdrop/service/bookdrop.service.ts +++ b/booklore-ui/src/app/features/bookdrop/service/bookdrop.service.ts @@ -56,6 +56,44 @@ export interface Page { number: number; } +export interface PatternExtractRequest { + pattern: string; + selectAll?: boolean; + excludedIds?: number[]; + selectedIds?: number[]; + preview?: boolean; +} + +export interface FileExtractionResult { + fileId: number; + fileName: string; + success: boolean; + extractedMetadata?: BookMetadata; + errorMessage?: string; +} + +export interface PatternExtractResult { + totalFiles: number; + successfullyExtracted: number; + failed: number; + results: FileExtractionResult[]; +} + +export interface BulkEditRequest { + fields: Partial; + enabledFields: string[]; + mergeArrays: boolean; + selectAll?: boolean; + excludedIds?: number[]; + selectedIds?: number[]; +} + +export interface BulkEditResult { + totalFiles: number; + successfullyUpdated: number; + failed: number; +} + @Injectable({providedIn: 'root'}) export class BookdropService { private readonly url = `${API_CONFIG.BASE_URL}/api/v1/bookdrop`; @@ -76,4 +114,12 @@ export class BookdropService { rescan(): Observable { return this.http.post(`${this.url}/rescan`, {}); } + + extractFromPattern(payload: PatternExtractRequest): Observable { + return this.http.post(`${this.url}/files/extract-pattern`, payload); + } + + bulkEditMetadata(payload: BulkEditRequest): Observable { + return this.http.post(`${this.url}/files/bulk-edit`, payload); + } } diff --git a/booklore-ui/src/app/features/dashboard/components/dashboard-settings/dashboard-settings.component.scss b/booklore-ui/src/app/features/dashboard/components/dashboard-settings/dashboard-settings.component.scss index caad078d..4f7ead61 100644 --- a/booklore-ui/src/app/features/dashboard/components/dashboard-settings/dashboard-settings.component.scss +++ b/booklore-ui/src/app/features/dashboard/components/dashboard-settings/dashboard-settings.component.scss @@ -1,13 +1,11 @@ .dashboard-settings { - width: 1000px; - max-width: 1200px; + max-width: 600px; min-height: 300px; padding: 2rem 1rem 0 1rem; margin: 0 auto; @media (max-width: 768px) { width: 100%; - max-width: 100%; padding: 3rem 1rem 0 1rem; box-sizing: border-box; } diff --git a/booklore-ui/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts b/booklore-ui/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts index 73fb0eeb..74d29a4a 100644 --- a/booklore-ui/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts +++ b/booklore-ui/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts @@ -14,7 +14,6 @@ import {ProgressSpinner} from 'primeng/progressspinner'; import {TooltipModule} from 'primeng/tooltip'; import {DashboardConfigService} from '../../services/dashboard-config.service'; import {ScrollerConfig, ScrollerType} from '../../models/dashboard-config.model'; -import {DashboardSettingsComponent} from '../dashboard-settings/dashboard-settings.component'; import {MagicShelfService} from '../../../magic-shelf/service/magic-shelf.service'; import {BookRuleEvaluatorService} from '../../../magic-shelf/service/book-rule-evaluator.service'; import {GroupRule} from '../../../magic-shelf/component/magic-shelf-component'; @@ -39,7 +38,6 @@ const DEFAULT_MAX_ITEMS = 20; standalone: true }) export class MainDashboardComponent implements OnInit { - ref: DynamicDialogRef | undefined | null; private bookService = inject(BookService); private dialogLauncher = inject(DialogLauncherService); @@ -203,14 +201,10 @@ export class MainDashboardComponent implements OnInit { } openDashboardSettings(): void { - this.ref = this.dialogLauncher.open({ - component: DashboardSettingsComponent, - header: 'Configure Dashboard', - showHeader: false - }); + this.dialogLauncher.openDashboardSettingsDialog(); } createNewLibrary() { - this.dialogLauncher.openLibraryCreatorDialog(); + this.dialogLauncher.openLibraryCreateDialog(); } } diff --git a/booklore-ui/src/app/features/library-creator/library-creator.component.ts b/booklore-ui/src/app/features/library-creator/library-creator.component.ts index 4ee1e495..32cb95af 100644 --- a/booklore-ui/src/app/features/library-creator/library-creator.component.ts +++ b/booklore-ui/src/app/features/library-creator/library-creator.component.ts @@ -1,6 +1,5 @@ import {Component, inject, OnInit} from '@angular/core'; -import {DialogService, DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {DirectoryPickerComponent} from '../../shared/components/directory-picker/directory-picker.component'; +import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; import {MessageService} from 'primeng/api'; import {Router} from '@angular/router'; import {LibraryService} from '../book/service/library.service'; @@ -15,6 +14,7 @@ import {IconPickerService, IconSelection} from '../../shared/service/icon-picker import {Select} from 'primeng/select'; import {Button} from 'primeng/button'; import {IconDisplayComponent} from '../../shared/components/icon-display/icon-display.component'; +import {DialogLauncherService} from '../../shared/services/dialog-launcher.service'; @Component({ selector: 'app-library-creator', @@ -31,7 +31,6 @@ export class LibraryCreatorComponent implements OnInit { mode!: string; library!: Library | undefined; editModeLibraryName: string = ''; - directoryPickerDialogRef!: DynamicDialogRef | null; watch: boolean = false; scanMode: LibraryScanMode = 'FILE_AS_BOOK'; defaultBookFormat: BookFileType | undefined = undefined; @@ -48,7 +47,7 @@ export class LibraryCreatorComponent implements OnInit { {label: 'CBX/CBZ/CBR', value: 'CBX'} ]; - private dialogService = inject(DialogService); + private dialogLauncherService = inject(DialogLauncherService); private dynamicDialogRef = inject(DynamicDialogRef); private dynamicDialogConfig = inject(DynamicDialogConfig); private libraryService = inject(LibraryService); @@ -69,7 +68,8 @@ export class LibraryCreatorComponent implements OnInit { if (iconType === 'CUSTOM_SVG') { this.selectedIcon = {type: 'CUSTOM_SVG', value: icon}; } else { - this.selectedIcon = {type: 'PRIME_NG', value: `pi pi-${icon}`}; + const value = icon.slice(0, 6) === 'pi pi-' ? icon : `pi pi-${icon}`; + this.selectedIcon = {type: 'PRIME_NG', value: value}; } this.watch = watch; @@ -85,16 +85,8 @@ export class LibraryCreatorComponent implements OnInit { } openDirectoryPicker(): void { - this.directoryPickerDialogRef = this.dialogService.open(DirectoryPickerComponent, { - header: 'Select Media Directory', - showHeader: false, - modal: true, - closable: true, - styleClass: 'dynamic-dialog-minimal', - contentStyle: {overflow: 'hidden'}, - baseZIndex: 10 - }); - this.directoryPickerDialogRef?.onClose.subscribe((selectedFolders: string[] | null) => { + const ref = this.dialogLauncherService.openDirectoryPickerDialog(); + ref?.onClose.subscribe((selectedFolders: string[] | null) => { if (selectedFolders && selectedFolders.length > 0) { selectedFolders.forEach(folder => { if (!this.folders.includes(folder)) { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html index 74070163..c38cc809 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/book-metadata-center.component.html @@ -20,11 +20,16 @@ - + + @if (admin || canEditMetadata) { - + + } @if (admin || canEditMetadata) { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html index 29a6f378..3a4aef33 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html @@ -335,184 +335,196 @@
-
-
- -
- - @if (!book.metadata!['reviewsLocked']) { - - } - @if (book.metadata!['reviewsLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['isbn10Locked']) { - - } - @if (book.metadata!['isbn10Locked']) { - - } -
-
-
- -
- - @if (!book.metadata!['isbn13Locked']) { - - } - @if (book.metadata!['isbn13Locked']) { - - } -
-
-
- -
- - @if (!book.metadata!['asinLocked']) { - - } - @if (book.metadata!['asinLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['amazonRatingLocked']) { - - } - @if (book.metadata!['amazonRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['amazonReviewCountLocked']) { - - } - @if (book.metadata!['amazonReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['googleIdLocked']) { - - } - @if (book.metadata!['googleIdLocked']) { - - } -
-
+
+
+ +
+ + @if (!book.metadata!['reviewsLocked']) { + + } + @if (book.metadata!['reviewsLocked']) { + + }
- -
-
- -
- - @if (!book.metadata!['goodreadsIdLocked']) { - - } - @if (book.metadata!['goodreadsIdLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['goodreadsRatingLocked']) { - - } - @if (book.metadata!['goodreadsRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['goodreadsReviewCountLocked']) { - - } - @if (book.metadata!['goodreadsReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverIdLocked']) { - - } - @if (book.metadata!['hardcoverIdLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverRatingLocked']) { - - } - @if (book.metadata!['hardcoverRatingLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['hardcoverReviewCountLocked']) { - - } - @if (book.metadata!['hardcoverReviewCountLocked']) { - - } -
-
-
- -
- - @if (!book.metadata!['comicvineIdLocked']) { - - } - @if (book.metadata!['comicvineIdLocked']) { - - } -
-
+
+
+ +
+ + @if (!book.metadata!['isbn10Locked']) { + + } + @if (book.metadata!['isbn10Locked']) { + + }
+
+
+ +
+ + @if (!book.metadata!['isbn13Locked']) { + + } + @if (book.metadata!['isbn13Locked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['asinLocked']) { + + } + @if (book.metadata!['asinLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['amazonRatingLocked']) { + + } + @if (book.metadata!['amazonRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['amazonReviewCountLocked']) { + + } + @if (book.metadata!['amazonReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['googleIdLocked']) { + + } + @if (book.metadata!['googleIdLocked']) { + + } +
+
+
-
-
- -
+
+
+ +
+ + @if (!book.metadata!['goodreadsIdLocked']) { + + } + @if (book.metadata!['goodreadsIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['goodreadsRatingLocked']) { + + } + @if (book.metadata!['goodreadsRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['goodreadsReviewCountLocked']) { + + } + @if (book.metadata!['goodreadsReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverIdLocked']) { + + } + @if (book.metadata!['hardcoverIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverBookIdLocked']) { + + } + @if (book.metadata!['hardcoverBookIdLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverRatingLocked']) { + + } + @if (book.metadata!['hardcoverRatingLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['hardcoverReviewCountLocked']) { + + } + @if (book.metadata!['hardcoverReviewCountLocked']) { + + } +
+
+
+ +
+ + @if (!book.metadata!['comicvineIdLocked']) { + + } + @if (book.metadata!['comicvineIdLocked']) { + + } +
+
+
+ +
+
+ +
- @if (!book.metadata!['descriptionLocked']) { - - } - @if (book.metadata!['descriptionLocked']) { - - } -
-
+ @if (!book.metadata!['descriptionLocked']) { + + } + @if (book.metadata!['descriptionLocked']) { + + }
+
+
@@ -545,6 +557,35 @@
} + @if (navigationState$ | async) { +
+ + + + {{ getNavigationPosition() }} + + + +
+ } +
+ @if (navigationState$ | async) { +
+ + + + {{ getNavigationPosition() }} + + + +
+ }
+
diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.scss b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.scss index aa6e675c..c592618c 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.scss +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.scss @@ -10,6 +10,10 @@ width: 250px; } +::ng-deep .cover-item p-image { + width: 150px; +} + ::ng-deep p-image img { margin: 0 auto; } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts index 6c806316..b9256ce4 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts @@ -14,7 +14,6 @@ import {BookService} from "../../../../book/service/book.service"; import {ProgressSpinner} from "primeng/progressspinner"; import {Tooltip} from "primeng/tooltip"; import {filter, take} from "rxjs/operators"; -import {DialogService} from "primeng/dynamicdialog"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {MetadataRefreshType} from "../../../model/request/metadata-refresh-type.enum"; import {AutoComplete} from "primeng/autocomplete"; @@ -22,8 +21,12 @@ import {DatePicker} from "primeng/datepicker"; import {Textarea} from "primeng/textarea"; import {Image} from "primeng/image"; import {LazyLoadImageModule} from "ng-lazyload-image"; -import {CoverSearchComponent} from '../../cover-search/cover-search.component'; import {TaskHelperService} from '../../../../settings/task-management/task-helper.service'; +import {BookDialogHelperService} from "../../../../book/components/book-browser/BookDialogHelperService"; +import {BookNavigationService} from '../../../../book/service/book-navigation.service'; +import {BookMetadataHostService} from '../../../../../shared/service/book-metadata-host-service'; +import {Router} from '@angular/router'; +import {UserService} from '../../../../settings/user-management/user.service'; @Component({ selector: "app-metadata-editor", @@ -61,7 +64,11 @@ export class MetadataEditorComponent implements OnInit { private bookService = inject(BookService); private taskHelperService = inject(TaskHelperService); protected urlHelper = inject(UrlHelperService); - private dialogService = inject(DialogService); + private bookDialogHelperService = inject(BookDialogHelperService); + private bookNavigationService = inject(BookNavigationService); + private metadataHostService = inject(BookMetadataHostService); + private router = inject(Router); + private userService = inject(UserService); private destroyRef = inject(DestroyRef); metadataForm: FormGroup; @@ -87,6 +94,9 @@ export class MetadataEditorComponent implements OnInit { filteredTags: string[] = []; filteredPublishers: string[] = []; filteredSeries: string[] = []; + private metadataCenterViewMode: 'route' | 'dialog' = 'route'; + + navigationState$ = this.bookNavigationService.getNavigationState(); filterCategories(event: { query: string }) { const query = event.query.toLowerCase(); @@ -153,6 +163,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRating: new FormControl(""), goodreadsReviewCount: new FormControl(""), hardcoverId: new FormControl(""), + hardcoverBookId: new FormControl(""), hardcoverRating: new FormControl(""), hardcoverReviewCount: new FormControl(""), googleId: new FormControl(""), @@ -182,6 +193,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRatingLocked: new FormControl(false), goodreadsReviewCountLocked: new FormControl(false), hardcoverIdLocked: new FormControl(false), + hardcoverBookIdLocked: new FormControl(false), hardcoverRatingLocked: new FormControl(false), hardcoverReviewCountLocked: new FormControl(false), googleIdLocked: new FormControl(false), @@ -207,6 +219,15 @@ export class MetadataEditorComponent implements OnInit { }); this.prepareAutoComplete(); + + this.userService.userState$ + .pipe( + filter(userState => !!userState?.user && userState.loaded), + take(1) + ) + .subscribe(userState => { + this.metadataCenterViewMode = userState.user?.userSettings.metadataCenterViewMode ?? 'route'; + }); } private prepareAutoComplete(): void { @@ -272,6 +293,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRating: metadata.goodreadsRating ?? null, goodreadsReviewCount: metadata.goodreadsReviewCount ?? null, hardcoverId: metadata.hardcoverId ?? null, + hardcoverBookId: metadata.hardcoverBookId ?? null, hardcoverRating: metadata.hardcoverRating ?? null, hardcoverReviewCount: metadata.hardcoverReviewCount ?? null, googleId: metadata.googleId ?? null, @@ -299,6 +321,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRatingLocked: metadata.goodreadsRatingLocked ?? false, goodreadsReviewCountLocked: metadata.goodreadsReviewCountLocked ?? false, hardcoverIdLocked: metadata.hardcoverIdLocked ?? false, + hardcoverBookIdLocked: metadata.hardcoverBookIdLocked ?? false, hardcoverRatingLocked: metadata.hardcoverRatingLocked ?? false, hardcoverReviewCountLocked: metadata.hardcoverReviewCountLocked ?? false, googleIdLocked: metadata.googleIdLocked ?? false, @@ -329,6 +352,7 @@ export class MetadataEditorComponent implements OnInit { {key: "goodreadsReviewCountLocked", control: "goodreadsReviewCount"}, {key: "goodreadsRatingLocked", control: "goodreadsRating"}, {key: "hardcoverIdLocked", control: "hardcoverId"}, + {key: "hardcoverBookIdLocked", control: "hardcoverBookId"}, {key: "hardcoverReviewCountLocked", control: "hardcoverReviewCount"}, {key: "hardcoverRatingLocked", control: "hardcoverRating"}, {key: "googleIdLocked", control: "googleId"}, @@ -467,6 +491,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRating: form.get("goodreadsRating")?.value, goodreadsReviewCount: form.get("goodreadsReviewCount")?.value, hardcoverId: form.get("hardcoverId")?.value, + hardcoverBookId: form.get("hardcoverBookId")?.value, hardcoverRating: form.get("hardcoverRating")?.value, hardcoverReviewCount: form.get("hardcoverReviewCount")?.value, googleId: form.get("googleId")?.value, @@ -498,6 +523,7 @@ export class MetadataEditorComponent implements OnInit { goodreadsRatingLocked: form.get("goodreadsRatingLocked")?.value, goodreadsReviewCountLocked: form.get("goodreadsReviewCountLocked")?.value, hardcoverIdLocked: form.get("hardcoverIdLocked")?.value, + hardcoverBookIdLocked: form.get("hardcoverBookIdLocked")?.value, hardcoverRatingLocked: form.get("hardcoverRatingLocked")?.value, hardcoverReviewCountLocked: form.get("hardcoverReviewCountLocked")?.value, googleIdLocked: form.get("googleIdLocked")?.value, @@ -684,21 +710,7 @@ export class MetadataEditorComponent implements OnInit { } openCoverSearch() { - const ref = this.dialogService.open(CoverSearchComponent, { - header: "Search Cover", - modal: true, - closable: true, - data: { - bookId: [this.currentBookId], - }, - style: { - width: "90vw", - height: "90vh", - maxWidth: "1200px", - position: "absolute", - }, - }); - + const ref = this.bookDialogHelperService.openCoverSearchDialog(this.currentBookId); ref?.onClose.subscribe((result) => { if (result) { this.metadataForm.get("thumbnailUrl")?.setValue(result); @@ -707,5 +719,43 @@ export class MetadataEditorComponent implements OnInit { }); } + canNavigatePrevious(): boolean { + return this.bookNavigationService.canNavigatePrevious(); + } + + canNavigateNext(): boolean { + return this.bookNavigationService.canNavigateNext(); + } + + navigatePrevious(): void { + const prevBookId = this.bookNavigationService.getPreviousBookId(); + if (prevBookId) { + this.navigateToBook(prevBookId); + } + } + + navigateNext(): void { + const nextBookId = this.bookNavigationService.getNextBookId(); + if (nextBookId) { + this.navigateToBook(nextBookId); + } + } + + private navigateToBook(bookId: number): void { + this.bookNavigationService.updateCurrentBook(bookId); + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/book', bookId], { + queryParams: {tab: 'edit'} + }); + } else { + this.metadataHostService.switchBook(bookId); + } + } + + getNavigationPosition(): string { + const position = this.bookNavigationService.getCurrentPosition(); + return position ? `${position.current} of ${position.total}` : ''; + } + protected readonly sample = sample; } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts index 509f56c0..ba1a88f2 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.ts @@ -73,6 +73,7 @@ export class MetadataPickerComponent implements OnInit { {label: 'GR Reviews', controlName: 'goodreadsReviewCount', lockedKey: 'goodreadsReviewCountLocked', fetchedKey: 'goodreadsReviewCount'}, {label: 'GR Rating', controlName: 'goodreadsRating', lockedKey: 'goodreadsRatingLocked', fetchedKey: 'goodreadsRating'}, {label: 'Hardcover ID', controlName: 'hardcoverId', lockedKey: 'hardcoverIdLocked', fetchedKey: 'hardcoverId'}, + {label: 'Hardcover Book ID', controlName: 'hardcoverBookId', lockedKey: 'hardcoverBookIdLocked', fetchedKey: 'hardcoverBookId'}, {label: 'HC Reviews', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'}, {label: 'HC Rating', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'}, {label: 'Google ID', controlName: 'googleId', lockedKey: 'googleIdLocked', fetchedKey: 'googleId'}, @@ -149,6 +150,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRating: new FormControl(''), goodreadsReviewCount: new FormControl(''), hardcoverId: new FormControl(''), + hardcoverBookId: new FormControl(''), hardcoverRating: new FormControl(''), hardcoverReviewCount: new FormControl(''), googleId: new FormControl(''), @@ -178,6 +180,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRatingLocked: new FormControl(false), goodreadsReviewCountLocked: new FormControl(false), hardcoverIdLocked: new FormControl(false), + hardcoverBookIdLocked: new FormControl(false), hardcoverRatingLocked: new FormControl(false), hardcoverReviewCountLocked: new FormControl(false), googleIdLocked: new FormControl(false), @@ -254,6 +257,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRating: metadata.goodreadsRating || null, goodreadsReviewCount: metadata.goodreadsReviewCount || null, hardcoverId: metadata.hardcoverId || null, + hardcoverBookId: metadata.hardcoverBookId || null, hardcoverRating: metadata.hardcoverRating || null, hardcoverReviewCount: metadata.hardcoverReviewCount || null, googleId: metadata.googleId || null, @@ -283,6 +287,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRatingLocked: metadata.goodreadsRatingLocked || false, goodreadsReviewCountLocked: metadata.goodreadsReviewCountLocked || false, hardcoverIdLocked: metadata.hardcoverIdLocked || false, + hardcoverBookIdLocked: metadata.hardcoverBookIdLocked || false, hardcoverRatingLocked: metadata.hardcoverRatingLocked || false, hardcoverReviewCountLocked: metadata.hardcoverReviewCountLocked || false, googleIdLocked: metadata.googleIdLocked || false, @@ -319,6 +324,7 @@ export class MetadataPickerComponent implements OnInit { if (metadata.goodreadsReviewCountLocked) this.metadataForm.get('goodreadsReviewCount')?.disable({emitEvent: false}); if (metadata.goodreadsRatingLocked) this.metadataForm.get('goodreadsRating')?.disable({emitEvent: false}); if (metadata.hardcoverIdLocked) this.metadataForm.get('hardcoverId')?.disable({emitEvent: false}); + if (metadata.hardcoverBookIdLocked) this.metadataForm.get('hardcoverBookId')?.disable({emitEvent: false}); if (metadata.hardcoverReviewCountLocked) this.metadataForm.get('hardcoverReviewCount')?.disable({emitEvent: false}); if (metadata.hardcoverRatingLocked) this.metadataForm.get('hardcoverRating')?.disable({emitEvent: false}); if (metadata.googleIdLocked) this.metadataForm.get('googleId')?.disable({emitEvent: false}); @@ -397,6 +403,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRating: this.metadataForm.get('goodreadsRating')?.value || this.copiedFields['goodreadsRating'] ? this.getNumberOrCopied('goodreadsRating') : null, goodreadsReviewCount: this.metadataForm.get('goodreadsReviewCount')?.value || this.copiedFields['goodreadsReviewCount'] ? this.getNumberOrCopied('goodreadsReviewCount') : null, hardcoverId: this.metadataForm.get('hardcoverId')?.value || this.copiedFields['hardcoverId'] ? this.getValueOrCopied('hardcoverId') : '', + hardcoverBookId: this.metadataForm.get('hardcoverBookId')?.value || this.copiedFields['hardcoverBookId'] ? (this.getNumberOrCopied('hardcoverBookId') ?? null) : null, hardcoverRating: this.metadataForm.get('hardcoverRating')?.value || this.copiedFields['hardcoverRating'] ? this.getNumberOrCopied('hardcoverRating') : null, hardcoverReviewCount: this.metadataForm.get('hardcoverReviewCount')?.value || this.copiedFields['hardcoverReviewCount'] ? this.getNumberOrCopied('hardcoverReviewCount') : null, googleId: this.metadataForm.get('googleId')?.value || this.copiedFields['googleId'] ? this.getValueOrCopied('googleId') : '', @@ -426,6 +433,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRatingLocked: this.metadataForm.get('goodreadsRatingLocked')?.value, goodreadsReviewCountLocked: this.metadataForm.get('goodreadsReviewCountLocked')?.value, hardcoverIdLocked: this.metadataForm.get('hardcoverIdLocked')?.value, + hardcoverBookIdLocked: this.metadataForm.get('hardcoverBookIdLocked')?.value, hardcoverRatingLocked: this.metadataForm.get('hardcoverRatingLocked')?.value, hardcoverReviewCountLocked: this.metadataForm.get('hardcoverReviewCountLocked')?.value, googleIdLocked: this.metadataForm.get('googleIdLocked')?.value, @@ -468,6 +476,7 @@ export class MetadataPickerComponent implements OnInit { goodreadsRating: current.goodreadsRating === null && original.goodreadsRating !== null, goodreadsReviewCount: current.goodreadsReviewCount === null && original.goodreadsReviewCount !== null, hardcoverId: !current.hardcoverId && !!original.hardcoverId, + hardcoverBookId: current.hardcoverBookId === null && original.hardcoverBookId !== null, hardcoverRating: current.hardcoverRating === null && original.hardcoverRating !== null, hardcoverReviewCount: current.hardcoverReviewCount === null && original.hardcoverReviewCount !== null, googleId: !current.googleId && !!original.googleId, diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html index 4c9907fc..149ad793 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.html @@ -46,6 +46,35 @@ }
+ + @if (navigationState$ | async) { + + }
@@ -454,27 +483,51 @@ @if (userService.userState$ | async; as userState) {
+ @if (navigationState$ | async) { + + + } @if (book!.bookType === 'PDF') { @if (readMenuItems$ | async; as readItems) { } } - @if (book!.bookType !== 'PDF') { + @if (book!.bookType !== 'PDF' && book!.bookType !== 'FB2') { } - + @if (userState.user!.permissions.canDownload || userState.user!.permissions.admin) { @if ((book!.alternativeFormats && book!.alternativeFormats.length > 0) || (book!.supplementaryFiles && book!.supplementaryFiles.length > 0)) { @if (downloadMenuItems$ | async; as downloadItems) { - + } } @else { - - } - } - @if (userState.user!.permissions.canEmailBook || userState.user!.permissions.admin) { - @if (emailMenuItems$ | async; as emailItems) { - + } } @if (userState.user!.permissions.canEditMetadata || userState.user!.permissions.admin) { @@ -488,7 +541,8 @@ (onClick)="quickRefresh(book!.id)" [disabled]="isAutoFetching" pTooltip="Automatically fetch metadata using default sources" - tooltipPosition="top"> + tooltipPosition="top" + styleClass="mobile-icon-only"> } } diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss index f7ad0a51..0bce931e 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.scss @@ -26,17 +26,21 @@ .cover-section { display: flex; + flex-direction: column; justify-content: center; + align-items: center; flex-shrink: 0; + gap: 1rem; @media (min-width: 768px) { justify-content: flex-start; + align-items: flex-start; } } .cover-wrapper { position: relative; - width: 175px; + width: 250px; overflow: hidden; @media (min-width: 768px) { @@ -461,6 +465,60 @@ display: flex; flex-wrap: wrap; gap: 0.5rem; + align-items: center; +} + +::ng-deep .action-divider { + display: none; + + @media (min-width: 768px) { + display: block; + } +} + +@media (max-width: 767px) { + ::ng-deep .mobile-icon-only .p-button-label { + display: none !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only .p-button { + min-width: auto !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only.p-splitbutton .p-button-label { + display: none !important; + padding: 10px !important; + } + + ::ng-deep .mobile-icon-only.p-splitbutton .p-button { + min-width: auto !important; + padding: 10px !important; + } +} + +.navigation-buttons-desktop { + display: none; + align-items: center; + gap: 0.5rem; +} + +.navigation-buttons-mobile { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +@media (min-width: 768px) { + .navigation-buttons-desktop { + display: flex; + } + + .navigation-buttons-mobile { + display: none; + } } .description-section { diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts index c2b900d0..462779b5 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-viewer/metadata-viewer.component.ts @@ -10,10 +10,8 @@ import {UrlHelperService} from '../../../../../shared/service/url-helper.service import {UserService} from '../../../../settings/user-management/user.service'; import {SplitButton} from 'primeng/splitbutton'; import {ConfirmationService, MenuItem, MessageService} from 'primeng/api'; -import {BookSenderComponent} from '../../../../book/components/book-sender/book-sender.component'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; +import {DynamicDialogRef} from 'primeng/dynamicdialog'; import {EmailService} from '../../../../settings/email-v2/email.service'; -import {ShelfAssignerComponent} from '../../../../book/components/shelf-assigner/shelf-assigner.component'; import {Tooltip} from 'primeng/tooltip'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {Editor} from 'primeng/editor'; @@ -29,13 +27,10 @@ import {DatePicker} from 'primeng/datepicker'; import {Tab, TabList, TabPanel, TabPanels, Tabs} from 'primeng/tabs'; import {BookReviewsComponent} from '../../../../book/components/book-reviews/book-reviews.component'; import {ProgressSpinner} from 'primeng/progressspinner'; - import {TieredMenu} from 'primeng/tieredmenu'; -import {AdditionalFileUploaderComponent} from '../../../../book/components/additional-file-uploader/additional-file-uploader.component'; import {Image} from 'primeng/image'; import {BookDialogHelperService} from '../../../../book/components/book-browser/BookDialogHelperService'; import {TagColor, TagComponent} from '../../../../../shared/components/tag/tag.component'; -import {MetadataFetchOptionsComponent} from '../../metadata-options-dialog/metadata-fetch-options/metadata-fetch-options.component'; import {BookNotesComponent} from '../../../../book/components/book-notes/book-notes-component'; import {TaskHelperService} from '../../../../settings/task-management/task-helper.service'; import { @@ -43,13 +38,16 @@ import { matchScoreRanges, pageCountRanges } from '../../../../book/components/book-browser/book-filter/book-filter.component'; +import {BookNavigationService} from '../../../../book/service/book-navigation.service'; +import {Divider} from 'primeng/divider'; +import {BookMetadataHostService} from '../../../../../shared/service/book-metadata-host-service'; @Component({ selector: 'app-metadata-viewer', standalone: true, templateUrl: './metadata-viewer.component.html', styleUrl: './metadata-viewer.component.scss', - imports: [Button, AsyncPipe, Rating, FormsModule, SplitButton, NgClass, Tooltip, DecimalPipe, Editor, ProgressBar, Menu, InfiniteScrollDirective, BookCardLiteComponent, DatePicker, Tab, TabList, TabPanel, TabPanels, Tabs, BookReviewsComponent, BookNotesComponent, ProgressSpinner, TieredMenu, Image, TagComponent, UpperCasePipe] + imports: [Button, AsyncPipe, Rating, FormsModule, SplitButton, NgClass, Tooltip, DecimalPipe, Editor, ProgressBar, Menu, InfiniteScrollDirective, BookCardLiteComponent, DatePicker, Tab, TabList, TabPanel, TabPanels, Tabs, BookReviewsComponent, BookNotesComponent, ProgressSpinner, TieredMenu, Image, TagComponent, UpperCasePipe, Divider] }) export class MetadataViewerComponent implements OnInit, OnChanges { @Input() book$!: Observable; @@ -57,7 +55,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { @ViewChild(Editor) quillEditor!: Editor; private originalRecommendedBooks: BookRecommendation[] = []; - private dialogService = inject(DialogService); + private bookDialogHelperService = inject(BookDialogHelperService) private emailService = inject(EmailService); private messageService = inject(MessageService); private bookService = inject(BookService); @@ -65,13 +63,11 @@ export class MetadataViewerComponent implements OnInit, OnChanges { protected urlHelper = inject(UrlHelperService); protected userService = inject(UserService); private confirmationService = inject(ConfirmationService); - private bookDialogHelperService = inject(BookDialogHelperService); private router = inject(Router); private destroyRef = inject(DestroyRef); private dialogRef?: DynamicDialogRef; - emailMenuItems$!: Observable; readMenuItems$!: Observable; refreshMenuItems$!: Observable; otherItems$!: Observable; @@ -97,26 +93,11 @@ export class MetadataViewerComponent implements OnInit, OnChanges { {value: ReadStatus.UNSET, label: 'Unset'}, ]; - ngOnInit(): void { - this.emailMenuItems$ = this.book$.pipe( - map(book => book?.metadata ?? null), - filter((metadata): metadata is BookMetadata => metadata != null), - map((metadata): MenuItem[] => [ - { - label: 'Custom Send', - command: () => { - this.dialogService.open(BookSenderComponent, { - header: 'Send Book to Email', - modal: true, - closable: true, - style: {position: 'absolute', top: '20%'}, - data: {bookId: metadata.bookId} - }); - } - } - ]) - ); + private bookNavigationService = inject(BookNavigationService); + private metadataHostService = inject(BookMetadataHostService); + navigationState$ = this.bookNavigationService.getNavigationState(); + ngOnInit(): void { this.refreshMenuItems$ = this.book$.pipe( filter((book): book is Book => book !== null), map((book): MenuItem[] => [ @@ -124,15 +105,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { label: 'Custom Fetch', icon: 'pi pi-sync', command: () => { - this.dialogService.open(MetadataFetchOptionsComponent, { - header: 'Metadata Refresh Options', - modal: true, - closable: true, - data: { - bookIds: [book.id], - metadataRefreshType: MetadataRefreshType.BOOKS, - }, - }); + this.bookDialogHelperService.openMetadataFetchOptionsDialog(book.id); } } ]) @@ -191,104 +164,123 @@ export class MetadataViewerComponent implements OnInit, OnChanges { this.otherItems$ = this.book$.pipe( filter((book): book is Book => book !== null), - map((book): MenuItem[] => { - const items: MenuItem[] = [ - { - label: 'Upload File', - icon: 'pi pi-upload', - command: () => { - this.dialogService.open(AdditionalFileUploaderComponent, { - header: 'Upload Additional File', - modal: true, - closable: true, - style: { - position: 'absolute', - top: '10%', + switchMap(book => + this.userService.userState$.pipe( + take(1), + map(userState => { + const items: MenuItem[] = [ + { + label: 'Upload File', + icon: 'pi pi-upload', + command: () => { + this.bookDialogHelperService.openAdditionalFileUploaderDialog(book); }, - data: {book} - }); - }, - }, - { - label: 'Organize Files', - icon: 'pi pi-arrows-h', - command: () => { - this.openFileMoverDialog(book.id); - }, - }, - { - label: 'Delete Book', - icon: 'pi pi-trash', - command: () => { - this.confirmationService.confirm({ - message: `Are you sure you want to delete "${book.metadata?.title}"?`, - header: 'Confirm Deletion', - icon: 'pi pi-exclamation-triangle', - acceptIcon: 'pi pi-trash', - rejectIcon: 'pi pi-times', - acceptButtonStyleClass: 'p-button-danger', - accept: () => { - this.bookService.deleteBooks(new Set([book.id])).subscribe({ - next: () => { - if (this.metadataCenterViewMode === 'route') { - this.router.navigate(['/dashboard']); - } else { - this.dialogRef?.close(); - } - }, - error: () => { + }, + { + label: 'Organize Files', + icon: 'pi pi-arrows-h', + command: () => { + this.openFileMoverDialog(book.id); + }, + }, + ]; + + // Add Send Book submenu if user has permission + if (userState?.user?.permissions.canEmailBook || userState?.user?.permissions.admin) { + items.push({ + label: 'Send Book', + icon: 'pi pi-send', + items: [ + { + label: 'Quick Send', + icon: 'pi pi-bolt', + command: () => this.quickSend(book.id) + }, + { + label: 'Custom Send', + icon: 'pi pi-cog', + command: () => { + this.bookDialogHelperService.openCustomSendDialog(book.id); } + } + ] + }); + } + + items.push({ + label: 'Delete Book', + icon: 'pi pi-trash', + command: () => { + this.confirmationService.confirm({ + message: `Are you sure you want to delete "${book.metadata?.title}"?`, + header: 'Confirm Deletion', + icon: 'pi pi-exclamation-triangle', + acceptIcon: 'pi pi-trash', + rejectIcon: 'pi pi-times', + acceptButtonStyleClass: 'p-button-danger', + accept: () => { + this.bookService.deleteBooks(new Set([book.id])).subscribe({ + next: () => { + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/dashboard']); + } else { + this.dialogRef?.close(); + } + }, + error: () => { + } + }); + } + }); + }, + }); + + // Add delete additional files menu if there are any additional files + if ((book.alternativeFormats && book.alternativeFormats.length > 0) || + (book.supplementaryFiles && book.supplementaryFiles.length > 0)) { + const deleteFileItems: MenuItem[] = []; + + // Add alternative formats + if (book.alternativeFormats && book.alternativeFormats.length > 0) { + book.alternativeFormats.forEach(format => { + const extension = this.getFileExtension(format.filePath); + deleteFileItems.push({ + label: `${format.fileName} (${this.getFileSizeInMB(format)})`, + icon: this.getFileIcon(extension), + command: () => this.deleteAdditionalFile(book.id, format.id, format.fileName || 'file') }); - } + }); + } + + // Add separator if both types exist + if (book.alternativeFormats && book.alternativeFormats.length > 0 && + book.supplementaryFiles && book.supplementaryFiles.length > 0) { + deleteFileItems.push({separator: true}); + } + + // Add supplementary files + if (book.supplementaryFiles && book.supplementaryFiles.length > 0) { + book.supplementaryFiles.forEach(file => { + const extension = this.getFileExtension(file.filePath); + deleteFileItems.push({ + label: `${file.fileName} (${this.getFileSizeInMB(file)})`, + icon: this.getFileIcon(extension), + command: () => this.deleteAdditionalFile(book.id, file.id, file.fileName || 'file') + }); + }); + } + + items.push({ + label: 'Delete Additional Files', + icon: 'pi pi-trash', + items: deleteFileItems }); - }, - }, - ]; + } - // Add delete additional files menu if there are any additional files - if ((book.alternativeFormats && book.alternativeFormats.length > 0) || - (book.supplementaryFiles && book.supplementaryFiles.length > 0)) { - const deleteFileItems: MenuItem[] = []; - - // Add alternative formats - if (book.alternativeFormats && book.alternativeFormats.length > 0) { - book.alternativeFormats.forEach(format => { - const extension = this.getFileExtension(format.filePath); - deleteFileItems.push({ - label: `${format.fileName} (${this.getFileSizeInMB(format)})`, - icon: this.getFileIcon(extension), - command: () => this.deleteAdditionalFile(book.id, format.id, format.fileName || 'file') - }); - }); - } - - // Add separator if both types exist - if (book.alternativeFormats && book.alternativeFormats.length > 0 && - book.supplementaryFiles && book.supplementaryFiles.length > 0) { - deleteFileItems.push({separator: true}); - } - - // Add supplementary files - if (book.supplementaryFiles && book.supplementaryFiles.length > 0) { - book.supplementaryFiles.forEach(file => { - const extension = this.getFileExtension(file.filePath); - deleteFileItems.push({ - label: `${file.fileName} (${this.getFileSizeInMB(file)})`, - icon: this.getFileIcon(extension), - command: () => this.deleteAdditionalFile(book.id, file.id, file.fileName || 'file') - }); - }); - } - - items.push({ - label: 'Delete Additional Files', - icon: 'pi pi-trash', - items: deleteFileItems - }); - } - - return items; - }) + return items; + }) + ) + ) ); this.userService.userState$ @@ -425,16 +417,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges { } assignShelf(bookId: number) { - this.dialogService.open(ShelfAssignerComponent, { - header: `Update Book's Shelves`, - showHeader: false, - dismissableMask: true, - modal: true, - closable: true, - contentStyle: {overflow: 'hidden'}, - baseZIndex: 10, - data: {book: this.bookService.getBookByIdFromState(bookId)} - }); + this.bookDialogHelperService.openShelfAssignerDialog(this.bookService.getBookByIdFromState(bookId), null); } updateReadStatus(status: ReadStatus): void { @@ -884,4 +867,42 @@ export class MetadataViewerComponent implements OnInit, OnChanges { protected readonly ResetProgressTypes = ResetProgressTypes; protected readonly ReadStatus = ReadStatus; + + canNavigatePrevious(): boolean { + return this.bookNavigationService.canNavigatePrevious(); + } + + canNavigateNext(): boolean { + return this.bookNavigationService.canNavigateNext(); + } + + navigatePrevious(): void { + const prevBookId = this.bookNavigationService.getPreviousBookId(); + if (prevBookId) { + this.navigateToBook(prevBookId); + } + } + + navigateNext(): void { + const nextBookId = this.bookNavigationService.getNextBookId(); + if (nextBookId) { + this.navigateToBook(nextBookId); + } + } + + private navigateToBook(bookId: number): void { + this.bookNavigationService.updateCurrentBook(bookId); + if (this.metadataCenterViewMode === 'route') { + this.router.navigate(['/book', bookId], { + queryParams: {tab: 'view'} + }); + } else { + this.metadataHostService.switchBook(bookId); + } + } + + getNavigationPosition(): string { + const position = this.bookNavigationService.getCurrentPosition(); + return position ? `${position.current} of ${position.total}` : ''; + } } diff --git a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.html b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.html index 43e92f88..70faaa90 100644 --- a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.html +++ b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.html @@ -312,6 +312,49 @@
+ +
+ +

+ Upload an image to set as the cover for all selected books. +

+
+ @if (selectedCoverFile) { +
+ + {{ selectedCoverFile.name }} + + +
+ } @else { + + + + } +
+
+
diff --git a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts index 4e274957..d8d4383d 100644 --- a/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts +++ b/booklore-ui/src/app/features/metadata/component/bulk-metadata-update/bulk-metadata-update-component.ts @@ -1,6 +1,6 @@ import {Component, inject, OnInit} from '@angular/core'; import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {CommonModule} from '@angular/common'; + import {InputText} from 'primeng/inputtext'; import {Button} from 'primeng/button'; import {Tooltip} from 'primeng/tooltip'; @@ -18,7 +18,6 @@ import {filter, take} from "rxjs/operators"; selector: 'app-bulk-metadata-update-component', standalone: true, imports: [ - CommonModule, ReactiveFormsModule, FormsModule, InputText, @@ -28,7 +27,7 @@ import {filter, take} from "rxjs/operators"; Checkbox, ProgressSpinner, AutoComplete - ], +], providers: [MessageService], templateUrl: './bulk-metadata-update-component.html', styleUrl: './bulk-metadata-update-component.scss' @@ -42,6 +41,7 @@ export class BulkMetadataUpdateComponent implements OnInit { mergeMoods = true; mergeTags = true; loading = false; + selectedCoverFile: File | null = null; clearFields = { authors: false, @@ -258,13 +258,37 @@ export class BulkMetadataUpdateComponent implements OnInit { this.loading = true; this.bookService.updateBooksMetadata(payload).subscribe({ next: () => { - this.loading = false; - this.messageService.add({ - severity: 'success', - summary: 'Metadata Updated', - detail: 'Books updated successfully' - }); - this.ref.close(true); + if (this.selectedCoverFile) { + this.bookService.bulkUploadCover(this.bookIds, this.selectedCoverFile).subscribe({ + next: () => { + this.loading = false; + this.messageService.add({ + severity: 'success', + summary: 'Metadata & Cover Updated', + detail: 'Books updated and cover upload started. Refresh the page when complete.' + }); + this.ref.close(true); + }, + error: err => { + console.error('Bulk cover upload failed:', err); + this.loading = false; + this.messageService.add({ + severity: 'warn', + summary: 'Partial Success', + detail: 'Metadata updated but cover upload failed' + }); + this.ref.close(true); + } + }); + } else { + this.loading = false; + this.messageService.add({ + severity: 'success', + summary: 'Metadata Updated', + detail: 'Books updated successfully' + }); + this.ref.close(true); + } }, error: err => { console.error('Bulk metadata update failed:', err); @@ -277,4 +301,15 @@ export class BulkMetadataUpdateComponent implements OnInit { } }); } + + onCoverFileSelect(event: Event): void { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + this.selectedCoverFile = input.files[0]; + } + } + + clearCoverFile(): void { + this.selectedCoverFile = null; + } } diff --git a/booklore-ui/src/app/features/metadata/component/metadata-review-dialog/metadata-review-dialog-component.html b/booklore-ui/src/app/features/metadata/component/metadata-review-dialog/metadata-review-dialog-component.html index f566117c..fe947ba5 100644 --- a/booklore-ui/src/app/features/metadata/component/metadata-review-dialog/metadata-review-dialog-component.html +++ b/booklore-ui/src/app/features/metadata/component/metadata-review-dialog/metadata-review-dialog-component.html @@ -1,10 +1,5 @@ @if (!loading) {
- -
-

Review Metadata Proposal

-
-
@if (currentProposal?.metadataJson; as proposed) { Search Metadata } - + @if (admin || canEditMetadata) {