Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9fc7596e | ||
|
|
3090a85726 | ||
|
|
347f2c3407 | ||
|
|
eaa2742d93 | ||
|
|
d714a1617f | ||
|
|
2a29bda34f | ||
|
|
ab6edd3196 | ||
|
|
6369aa158f | ||
|
|
be7e79be55 | ||
|
|
baf14da2c8 | ||
|
|
35acd4e0f7 | ||
|
|
c9e1b8eec5 | ||
|
|
24a7a2f547 | ||
|
|
c82dba1a1b | ||
|
|
8175ee8524 | ||
|
|
8a05bad2aa | ||
|
|
e2257565db | ||
|
|
eba93a3b6c | ||
|
|
801e372d4c | ||
|
|
03e81a0f16 | ||
|
|
f9ac1bd62e | ||
|
|
8aa6aff53a | ||
|
|
458bb641b5 | ||
|
|
247f82b5a7 | ||
|
|
06b4c147bd | ||
|
|
25154e4dbe | ||
|
|
989eea9e24 | ||
|
|
510e1c38eb | ||
|
|
ec05c5c049 | ||
|
|
d86a13cc89 | ||
|
|
85dedf4b28 | ||
|
|
0fa53185f2 | ||
|
|
3dd4a33595 | ||
|
|
fe2bfd8739 | ||
|
|
a15b0e33d5 | ||
|
|
0a242f6a57 | ||
|
|
3c46010155 | ||
|
|
02189a67bd | ||
|
|
d4985f57d4 | ||
|
|
4ab66fdf14 | ||
|
|
c19abe0da7 | ||
|
|
dd6b7968db | ||
|
|
2228ae7197 | ||
|
|
30987dcad2 | ||
|
|
43beadbdcb | ||
|
|
416d79aed3 | ||
|
|
abc0988fdf | ||
|
|
c7e0ea5b5b | ||
|
|
b35447934e | ||
|
|
d1e82eb8f1 | ||
|
|
7d44cc1a40 |
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script processes JSON translation files for localization checks. It compares translation files in a branch with
|
||||
a reference file to ensure consistency. The script performs two main checks:
|
||||
1. Verifies that the number of translation keys in the translation files matches the reference file.
|
||||
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
||||
|
||||
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python check_language_json.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
"""
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_json.py --reference-file frontend/public/locales/en-GB/translation.json --branch "" --files frontend/public/locales/de-DE/translation.json frontend/public/locales/fr-FR/translation.json
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
"""
|
||||
Identifies duplicate keys in a JSON file (including nested keys).
|
||||
:param file_path: Path to the JSON file.
|
||||
:param keys: Dictionary to track keys (used for recursion).
|
||||
:param prefix: Prefix for nested keys.
|
||||
:return: List of tuples (key, first_occurrence_path, duplicate_path).
|
||||
"""
|
||||
if keys is None:
|
||||
keys = {}
|
||||
|
||||
duplicates = []
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def process_dict(obj, current_prefix=""):
|
||||
for key, value in obj.items():
|
||||
full_key = f"{current_prefix}.{key}" if current_prefix else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
process_dict(value, full_key)
|
||||
else:
|
||||
if full_key in keys:
|
||||
duplicates.append((full_key, keys[full_key], full_key))
|
||||
else:
|
||||
keys[full_key] = full_key
|
||||
|
||||
process_dict(data, prefix)
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for JSON files (e.g., 500 KB)
|
||||
MAX_FILE_SIZE = 500 * 1024
|
||||
|
||||
|
||||
def parse_json_file(file_path):
|
||||
"""
|
||||
Parses a JSON translation file and returns a flat dictionary of all keys.
|
||||
:param file_path: Path to the JSON file.
|
||||
:return: Dictionary with flattened keys.
|
||||
"""
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def flatten_dict(d, parent_key="", sep="."):
|
||||
items = {}
|
||||
for k, v in d.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
if isinstance(v, dict):
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
else:
|
||||
items[new_key] = v
|
||||
return items
|
||||
|
||||
return flatten_dict(data)
|
||||
|
||||
|
||||
def unflatten_dict(d, sep="."):
|
||||
"""
|
||||
Converts a flat dictionary with dot notation keys back to nested dict.
|
||||
:param d: Flattened dictionary.
|
||||
:param sep: Separator used in keys.
|
||||
:return: Nested dictionary.
|
||||
"""
|
||||
result = {}
|
||||
for key, value in d.items():
|
||||
parts = key.split(sep)
|
||||
current = result
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
return result
|
||||
|
||||
|
||||
def write_json_file(file_path, updated_properties):
|
||||
"""
|
||||
Writes updated properties back to the JSON file.
|
||||
:param file_path: Path to the JSON file.
|
||||
:param updated_properties: Dictionary of updated properties to write.
|
||||
"""
|
||||
nested_data = unflatten_dict(updated_properties)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
json.dump(nested_data, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n") # Add trailing newline
|
||||
|
||||
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
"""
|
||||
Updates missing keys in the translation files based on the reference file.
|
||||
:param reference_file: Path to the reference JSON file.
|
||||
:param file_list: List of translation files to update.
|
||||
:param branch: Branch where the files are located.
|
||||
"""
|
||||
reference_properties = parse_json_file(reference_file)
|
||||
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".json")
|
||||
or not os.path.dirname(file_path).endswith("locales")
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_json_file(os.path.join(branch, file_path))
|
||||
updated_properties = {}
|
||||
|
||||
for ref_key, ref_value in reference_properties.items():
|
||||
if ref_key in current_properties:
|
||||
# Keep the current translation
|
||||
updated_properties[ref_key] = current_properties[ref_key]
|
||||
else:
|
||||
# Add missing key with reference value
|
||||
updated_properties[ref_key] = ref_value
|
||||
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
|
||||
|
||||
def read_json_keys(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
return parse_json_file(file_path)
|
||||
return {}
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
reference_branch = branch
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_keys = read_json_keys(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
|
||||
file_arr = file_list
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.getcwd(), "frontend", "public", "locales")
|
||||
)
|
||||
|
||||
for file_path in file_arr:
|
||||
file_normpath = os.path.normpath(file_path)
|
||||
absolute_path = os.path.abspath(file_normpath)
|
||||
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.startswith(base_dir):
|
||||
raise ValueError(f"Unsafe file found: {file_normpath}")
|
||||
|
||||
# Verify file size before processing
|
||||
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"The file {file_normpath} is too large and could pose a security risk."
|
||||
)
|
||||
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
|
||||
locale_dir = os.path.basename(os.path.dirname(file_normpath))
|
||||
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
and locale_dir == "en-GB"
|
||||
):
|
||||
continue
|
||||
|
||||
if not file_normpath.endswith(".json") or basename_current_file != "translation.json":
|
||||
continue
|
||||
|
||||
only_reference_file = False
|
||||
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
|
||||
current_keys = read_json_keys(os.path.join(branch, file_path))
|
||||
reference_key_count = len(reference_keys)
|
||||
current_key_count = len(current_keys)
|
||||
|
||||
if reference_key_count != current_key_count:
|
||||
report.append("")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
has_differences = True
|
||||
if reference_key_count > current_key_count:
|
||||
report.append(
|
||||
f" - **_Mismatched key count_**: {reference_key_count} (reference) vs {current_key_count} (current). Translation keys are missing."
|
||||
)
|
||||
elif reference_key_count < current_key_count:
|
||||
report.append(
|
||||
f" - **_Too many keys_**: {reference_key_count} (reference) vs {current_key_count} (current). Please verify if there are additional keys that need to be removed."
|
||||
)
|
||||
else:
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys_set = set(current_keys.keys())
|
||||
reference_keys_set = set(reference_keys.keys())
|
||||
missing_keys = current_keys_set.difference(reference_keys_set)
|
||||
extra_keys = reference_keys_set.difference(current_keys_set)
|
||||
missing_keys_list = list(missing_keys)
|
||||
extra_keys_list = list(extra_keys)
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
has_differences = True
|
||||
missing_keys_str = "`, `".join(missing_keys_list)
|
||||
extra_keys_str = "`, `".join(extra_keys_list)
|
||||
report.append("2. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
if missing_keys_list:
|
||||
report.append(
|
||||
f" - **_Extra keys in `{locale_dir}/{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{locale_dir}/{basename_current_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
else:
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
if find_duplicate_keys(os.path.join(branch, file_normpath)):
|
||||
has_differences = True
|
||||
output = "\n".join(
|
||||
[
|
||||
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
|
||||
for key, first, duplicate in find_duplicate_keys(
|
||||
os.path.join(branch, file_normpath)
|
||||
)
|
||||
]
|
||||
)
|
||||
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
report.append(" - duplicate entries were found:")
|
||||
report.append(output)
|
||||
else:
|
||||
report.append("3. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
report.append("")
|
||||
report.append("---")
|
||||
report.append("")
|
||||
|
||||
if has_differences:
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.json](https://github.com/Stirling-Tools/Stirling-PDF/blob/V2/frontend/public/locales/en-GB/translation.json)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
help="Actor from PR.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-file",
|
||||
required=True,
|
||||
help="Path to the reference file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--branch",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Branch name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-file",
|
||||
type=str,
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--files",
|
||||
nargs="+",
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Sanitize --actor input to avoid injection attacks
|
||||
if args.actor:
|
||||
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||
|
||||
# Sanitize --branch input to avoid injection attacks
|
||||
if args.branch:
|
||||
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||
|
||||
file_list = args.files
|
||||
if file_list is None:
|
||||
if args.check_file:
|
||||
file_list = [args.check_file]
|
||||
else:
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"frontend",
|
||||
"public",
|
||||
"locales",
|
||||
"*",
|
||||
"translation.json",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- ".github/workflows/frontend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
@@ -28,12 +29,12 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
@@ -56,9 +57,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
@@ -114,7 +115,7 @@ jobs:
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -167,7 +168,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Sync Files V2
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- syncLangTest
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.json"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-files:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Sync translation JSON files
|
||||
run: |
|
||||
python .github/scripts/check_language_json.py --reference-file "frontend/public/locales/en-GB/translation.json" --branch V2
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add frontend/public/locales/*/translation.json
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation_v2.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
git add README.md scripts/ignore_translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: Update files
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: sync_readme_v2
|
||||
base: V2
|
||||
title: ":globe_with_meridians: [V2] Sync Translations + Update README Progress Table"
|
||||
body: |
|
||||
### Description of Changes
|
||||
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation for the **V2 branch**. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`.
|
||||
- Ensured consistency and synchronization across all supported language files.
|
||||
- Highlighted any missing or incomplete translations.
|
||||
|
||||
#### **2. Update README.md**
|
||||
- Generated the translation progress table in `README.md`.
|
||||
- Added a summary of the current translation status for all supported languages.
|
||||
- Included up-to-date statistics on translation coverage.
|
||||
|
||||
#### **Why these changes are necessary**
|
||||
- Keeps translation files aligned with the latest reference updates.
|
||||
- Ensures the documentation reflects the current translation progress.
|
||||
|
||||
---
|
||||
|
||||
Auto-generated by [create-pull-request][1].
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
draft: false
|
||||
delete-branch: true
|
||||
labels: github-actions
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
frontend/public/locales/*/translation.json
|
||||
@@ -192,6 +192,11 @@ return useToolOperation({
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Translation Rules
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
|
||||
- Translation files are located in `frontend/public/locales/`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
|
||||
@@ -97,7 +97,6 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
|
||||
|
||||
|
||||
|
||||
# 📖 Get Started
|
||||
|
||||
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for:
|
||||
@@ -116,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@@ -258,12 +258,6 @@ public class AppConfig {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "GoogleDriveEnabled")
|
||||
@Profile("default")
|
||||
public boolean googleDriveEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "license")
|
||||
@Profile("default")
|
||||
public String licenseType() {
|
||||
|
||||
@@ -530,7 +530,6 @@ public class ApplicationProperties {
|
||||
private boolean ssoAutoLogin;
|
||||
private boolean database;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
private GoogleDrive googleDrive = new GoogleDrive();
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
@@ -549,26 +548,6 @@ public class ApplicationProperties {
|
||||
: producer;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class GoogleDrive {
|
||||
private boolean enabled;
|
||||
private String clientId;
|
||||
private String apiKey;
|
||||
private String appId;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId == null || clientId.trim().isEmpty() ? "" : clientId;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey == null || apiKey.trim().isEmpty() ? "" : apiKey;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId == null || appId.trim().isEmpty() ? "" : appId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
-16
@@ -109,22 +109,6 @@ class ApplicationPropertiesLogicTest {
|
||||
assertTrue(ex.getMessage().toLowerCase().contains("not supported"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void premium_google_drive_getters_return_empty_string_on_null_or_blank() {
|
||||
Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive();
|
||||
|
||||
assertEquals("", gd.getClientId());
|
||||
assertEquals("", gd.getApiKey());
|
||||
assertEquals("", gd.getAppId());
|
||||
|
||||
gd.setClientId(" id ");
|
||||
gd.setApiKey(" key ");
|
||||
gd.setAppId(" app ");
|
||||
assertEquals(" id ", gd.getClientId());
|
||||
assertEquals(" key ", gd.getApiKey());
|
||||
assertEquals(" app ", gd.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ui_getters_return_null_for_blank() {
|
||||
ApplicationProperties.Ui ui = new ApplicationProperties.Ui();
|
||||
|
||||
-5
@@ -98,11 +98,6 @@ public class ConfigController {
|
||||
if (applicationContext.containsBean("license")) {
|
||||
configData.put("license", applicationContext.getBean("license", String.class));
|
||||
}
|
||||
if (applicationContext.containsBean("GoogleDriveEnabled")) {
|
||||
configData.put(
|
||||
"GoogleDriveEnabled",
|
||||
applicationContext.getBean("GoogleDriveEnabled", Boolean.class));
|
||||
}
|
||||
if (applicationContext.containsBean("SSOAutoLogin")) {
|
||||
configData.put(
|
||||
"SSOAutoLogin",
|
||||
|
||||
@@ -1916,6 +1916,7 @@ fileManager.storageError=Storage error occurred
|
||||
fileManager.storageLow=Storage is running low. Consider removing old files.
|
||||
fileManager.uploadError=Failed to upload some files.
|
||||
fileManager.supportMessage=Powered by browser database storage for unlimited capacity
|
||||
fileManager.loadingFiles=Loading files...
|
||||
|
||||
# Page Editor
|
||||
pageEditor.title=Page Editor
|
||||
|
||||
@@ -76,11 +76,6 @@ premium:
|
||||
author: username
|
||||
creator: Stirling-PDF
|
||||
producer: Stirling-PDF
|
||||
googleDrive:
|
||||
enabled: false
|
||||
clientId: ''
|
||||
apiKey: ''
|
||||
appId: ''
|
||||
enterpriseFeatures:
|
||||
audit:
|
||||
enabled: true # Enable audit logging
|
||||
|
||||
@@ -422,10 +422,6 @@
|
||||
<span th:text="#{fileChooser.or}" style="margin: 0 5px;"></span>
|
||||
<span th:text="#{fileChooser.dragAndDrop}" id="dragAndDrop"></span>
|
||||
</div>
|
||||
<hr th:if="${@GoogleDriveEnabled == true}" class="horizontal-divider" >
|
||||
</div>
|
||||
<div th:if="${@GoogleDriveEnabled == true}" th:id="${name}+'-google-drive-button'" class="google-drive-button" th:attr="data-name=${name}, data-multiple=${!disableMultipleFiles}, data-accept=${accept}" >
|
||||
<img th:src="@{'/images/google-drive.svg'}" alt="google drive">
|
||||
</div>
|
||||
</div>
|
||||
<div class="selected-files flex-wrap"></div>
|
||||
@@ -443,16 +439,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{'/js/fileInput.js'}" type="module"></script>
|
||||
|
||||
<div th:if="${@GoogleDriveEnabled == true}" >
|
||||
<script type="text/javascript" th:src="@{'/js/googleFilePicker.js'}"></script>
|
||||
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
|
||||
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
|
||||
|
||||
<script th:inline="javascript">
|
||||
window.stirlingPDF.GoogleDriveClientId = /*[[${@GoogleDriveConfig.getClientId()}]]*/ null;
|
||||
window.stirlingPDF.GoogleDriveApiKey = /*[[${@GoogleDriveConfig.getApiKey()}]]*/ null;
|
||||
window.stirlingPDF.GoogleDriveAppId = /*[[${@GoogleDriveConfig.getAppId()}]]*/ null;
|
||||
</script>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
-14
@@ -12,7 +12,6 @@ import org.springframework.core.annotation.Order;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.EnterpriseEdition;
|
||||
import stirling.software.common.model.ApplicationProperties.Premium;
|
||||
import stirling.software.common.model.ApplicationProperties.Premium.ProFeatures.GoogleDrive;
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@@ -55,19 +54,6 @@ public class EEAppConfig {
|
||||
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
|
||||
}
|
||||
|
||||
@Profile("security")
|
||||
@Bean(name = "GoogleDriveEnabled")
|
||||
@Primary
|
||||
public boolean googleDriveEnabled() {
|
||||
return runningProOrHigher()
|
||||
&& applicationProperties.getPremium().getProFeatures().getGoogleDrive().isEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "GoogleDriveConfig")
|
||||
public GoogleDrive googleDriveConfig() {
|
||||
return applicationProperties.getPremium().getProFeatures().getGoogleDrive();
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
@SuppressWarnings("deprecation")
|
||||
public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) {
|
||||
|
||||
+8
-1
@@ -50,7 +50,14 @@ docker-compose -f docker/compose/docker-compose.fat.yml up --build
|
||||
- **Custom Ports**: Modify port mappings in docker-compose files
|
||||
- **Memory Limits**: Adjust memory limits per variant (2G ultra-lite, 4G standard, 6G fat)
|
||||
|
||||
### [Google Drive Integration](https://developers.google.com/workspace/drive/picker/guides/overview)
|
||||
|
||||
- **VITE_GOOGLE_DRIVE_CLIENT_ID**: [OAuth 2.0 Client ID](https://console.cloud.google.com/auth/clients/create)
|
||||
- **VITE_GOOGLE_DRIVE_API_KEY**: [Create New API](https://console.cloud.google.com/apis)
|
||||
- **VITE_GOOGLE_DRIVE_APP_ID**: This is your [project number](https://console.cloud.google.com/iam-admin/settings) in the GoogleCloud Settings
|
||||
|
||||
## Development vs Production
|
||||
|
||||
- **Development**: Keep backend port 8080 exposed for debugging
|
||||
- **Production**: Remove backend port exposure, use only frontend proxy
|
||||
- **Production**: Remove backend port exposure, use only frontend proxy
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ services:
|
||||
- "3000:80"
|
||||
environment:
|
||||
BACKEND_URL: http://backend:8080
|
||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -44,6 +44,9 @@ services:
|
||||
- "3000:80"
|
||||
environment:
|
||||
BACKEND_URL: http://backend:8080
|
||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -46,6 +46,9 @@ services:
|
||||
- "3000:80"
|
||||
environment:
|
||||
BACKEND_URL: http://backend:8080
|
||||
#VITE_GOOGLE_DRIVE_CLIENT_ID: <INSERT_YOUR_CLIENT_ID_HERE>
|
||||
#VITE_GOOGLE_DRIVE_API_KEY: <INSERT_YOUR_API_KEY_HERE>
|
||||
#VITE_GOOGLE_DRIVE_APP_ID: <INSERT_YOUR_APP_ID_HERE>
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY frontend/package*.json ./
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import globals from "globals";
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const srcGlobs = [
|
||||
'src/**/*.{js,mjs,jsx,ts,tsx}',
|
||||
];
|
||||
const nodeGlobs = [
|
||||
'scripts/**/*.{js,ts,mjs}',
|
||||
'*.config.{js,ts,mjs}',
|
||||
];
|
||||
|
||||
export default defineConfig(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
@@ -15,7 +24,6 @@ export default defineConfig(
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"no-undef": "off", // Temporarily disabled until codebase conformant
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
@@ -38,5 +46,23 @@ export default defineConfig(
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
// Config for browser scripts
|
||||
{
|
||||
files: srcGlobs,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
}
|
||||
}
|
||||
},
|
||||
// Config for node scripts
|
||||
{
|
||||
files: nodeGlobs,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Generated
+258
-121
@@ -10,22 +10,24 @@
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@embedpdf/core": "^1.3.0",
|
||||
"@embedpdf/engines": "^1.2.1",
|
||||
"@embedpdf/plugin-export": "^1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||
"@embedpdf/plugin-loader": "^1.3.0",
|
||||
"@embedpdf/plugin-pan": "^1.3.0",
|
||||
"@embedpdf/plugin-render": "^1.3.0",
|
||||
"@embedpdf/plugin-rotate": "^1.3.0",
|
||||
"@embedpdf/plugin-scroll": "^1.3.0",
|
||||
"@embedpdf/plugin-search": "^1.3.0",
|
||||
"@embedpdf/plugin-selection": "^1.3.0",
|
||||
"@embedpdf/plugin-spread": "^1.3.0",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.0",
|
||||
"@embedpdf/plugin-tiling": "^1.3.0",
|
||||
"@embedpdf/plugin-viewport": "^1.3.0",
|
||||
"@embedpdf/plugin-zoom": "^1.3.0",
|
||||
"@embedpdf/core": "^1.3.1",
|
||||
"@embedpdf/engines": "^1.3.1",
|
||||
"@embedpdf/plugin-annotation": "^1.3.1",
|
||||
"@embedpdf/plugin-export": "^1.3.1",
|
||||
"@embedpdf/plugin-history": "^1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.1",
|
||||
"@embedpdf/plugin-loader": "^1.3.1",
|
||||
"@embedpdf/plugin-pan": "^1.3.1",
|
||||
"@embedpdf/plugin-render": "^1.3.1",
|
||||
"@embedpdf/plugin-rotate": "^1.3.1",
|
||||
"@embedpdf/plugin-scroll": "^1.3.1",
|
||||
"@embedpdf/plugin-search": "^1.3.1",
|
||||
"@embedpdf/plugin-selection": "^1.3.1",
|
||||
"@embedpdf/plugin-spread": "^1.3.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.1",
|
||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -39,6 +41,7 @@
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.12.2",
|
||||
"globals": "^16.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
@@ -63,6 +66,10 @@
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/gapi.client.drive-v3": "^0.0.5",
|
||||
"@types/google.accounts": "^0.0.18",
|
||||
"@types/google.picker": "^0.0.51",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
@@ -489,13 +496,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-KEic1NA9JrtNRoTq3O3m93YTglRKweR6uqjzX3sLGCmy+LsUjiH5WOCJAztlSlmZEXysAlZlyzG/09gz4tpBAg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz",
|
||||
"integrity": "sha512-2Az6trhiMMBIv+GFvV8H8UOS1gwQn7NK0KaJMcdsZbUHYLO0P95aVd6Pi/GRzEH4XyF51TDIoTOAUtf07TQ5dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "1.3.0",
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/engines": "1.3.1",
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -505,13 +512,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/engines": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.0.tgz",
|
||||
"integrity": "sha512-6WbYwxtCCjOazEMGKbhKRkos6S1VkzI4R2u6dUuIsUw9G2HLP4bwJCBKj9A0FuMAJkKQ3VL5eVCSGfqaCaRoyQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.1.tgz",
|
||||
"integrity": "sha512-G3pI+18la7spviUMuA5s9/hV95jlfkA2+CNxqlHBO5ocw3641E3d36Lv+mx+6yU7k0B5vEOQPZDGRMg7KFziBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0",
|
||||
"@embedpdf/pdfium": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"@embedpdf/pdfium": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -521,27 +528,63 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/models": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.0.tgz",
|
||||
"integrity": "sha512-LIY6T+nQoc1hi6nq1NlH6sR43J3PYOg9Bux8ouEnKjEGiZMgyd1cMxhBfrrY+Ft6DsSkqqujFOVEwjeYQYy3dg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.1.tgz",
|
||||
"integrity": "sha512-OzmO1rQAuOP/Y3aYXmW21dPNAx49olhr9ZO2hDdI0fbNBHTVGxnaKqOISxVmUz7TmhTwVBljERACnaA8Ib4b4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/pdfium": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.0.tgz",
|
||||
"integrity": "sha512-rSBFYjxwQ58L/HcqR0l5Vv4G5t+CCOKlFYrDReTZYNN7fhzKPUWbXUn4ARahZWCNmF8svHumV2P4ArakJJviuw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.1.tgz",
|
||||
"integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.0.tgz",
|
||||
"integrity": "sha512-R6VItLmXmXbb0/4AsH1YGUZd0c64K/8kxQd0XAvgUJwcL7Z4s8bLsqRx4sVQqwVllaPEJbAdFn1CC/ymkGB+fg==",
|
||||
"node_modules/@embedpdf/plugin-annotation": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.1.tgz",
|
||||
"integrity": "sha512-mmePRYYBB8v8NIZ95XVfFkpyQ2QiKIGdWyvrPeJXSbL3/K6d6ix+o/jHBVvBWyTsQzdIlzs+FW8+iT0M1zkEow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"@embedpdf/utils": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-history": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-selection": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.1.tgz",
|
||||
"integrity": "sha512-reb03vNPFP5GuIAFExMcuYBVYu/deVO2v8EoCwRZ/lzzYMORIkJjpNWDQPo9VfyGBh1x4/o3CHvxisU1Y1tDLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-history": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.1.tgz",
|
||||
"integrity": "sha512-HrPkWQmAk08mbHiOcIN4htVq5KJMqI9zSjAqaYQEhV/TugeHfWVpK+xMst/PzuFb14HWgk5gWXjtV5E4SDlw9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -549,15 +592,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.0.tgz",
|
||||
"integrity": "sha512-iMG7mW+4YpNjBeSAcC5kK9VnjwmNu71HTxVtKnN73t3EBfukbMH4y7Tp2ds+4I97H6vc18RK5xuUCSesEOBgww==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.1.tgz",
|
||||
"integrity": "sha512-8h3y5a9tQ1fZlc4mP1/+XKyuHWwcQEm9AujKxy+6f6omtCBzpnKrH95bURgYOzQEBGY7d5C3HvG6JOlh0o1x3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -565,15 +608,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-loader": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.0.tgz",
|
||||
"integrity": "sha512-tkOa1UwFOimueSxxm2hRAAh64K75itDvUO6wHjb5X5s0Hx4DccfrJ7KusDhxBkeQLFXtZknPG0Q2/9T+joAqeQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.1.tgz",
|
||||
"integrity": "sha512-NjNmA7TOs3E/zwb9I+YohzyGkxq8y5NUGu0MKgh2g41lZoFvyqTAjFPar+RjEiLX8iiJiwNZswyJsNrytmS3Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -581,17 +624,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-pan": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.0.tgz",
|
||||
"integrity": "sha512-tZxUpX9dvd/VDHCTqM9Yjss4M8pkJWFUA5GDNmPkExRXIASuB98wEP8fh0rQt13TEZ30rV77cEsNXngju56kjg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.1.tgz",
|
||||
"integrity": "sha512-lF1gkz/a77G3+Rr8MOefkGnPJ1i5xWnClXm2ZzYAl7PbOScp59/PaP7qeU7eMPC4FHQM81ZhCgVYGXogbaB8ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -599,15 +642,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-render": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.0.tgz",
|
||||
"integrity": "sha512-ZyxoGIIUa2HBLt1IB64EdWqBxHh01AX/1HJ7/cnoQK1h/oKXRbMAX6Mb23JCh2PGa4sGeyV3psoFMt037Eew3g==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.1.tgz",
|
||||
"integrity": "sha512-c9oH097e1CVUpYF9RgZRfV/7XCJ0pf+svdT1wyM2MbWby06ti20oCwT9wf7BLY0hPQ7+eO3wunr1I1/y3MnVrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -615,15 +658,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-rotate": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.0.tgz",
|
||||
"integrity": "sha512-EyLLwf9VKQCsMRTe0KwGe+ZAaFqmcYS5WW/qqPBNfvSuBaybNpdI+C72IQFr41X7cYQV58OgEL3bfDb1MBPGHQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.1.tgz",
|
||||
"integrity": "sha512-mRAlIW7IZAnCyDuYqN13yDc6yoNIYLUB4uYTUAR7vTIt021C8H5jDHk9TmLwcH0tQ8/R3yHuDm/XPAe0zfs81g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -631,16 +674,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-scroll": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.0.tgz",
|
||||
"integrity": "sha512-o1n6Mkoc92BHAkoCX0mSLXgOj4uAkokNbvP+2QMijShzTsl95gU5UzK6siZ5o6WgZBznJcceYmLuPR5ODqZDWg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.1.tgz",
|
||||
"integrity": "sha512-mDvK3DyBZC8/8pOEdJsWtSjCmV2ZuZJJ6xfspJpsaDVywo1Vq6M55BtKThkhqED6mqbFWTN9rP9cbWG8KDBWVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -648,16 +691,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-search": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.0.tgz",
|
||||
"integrity": "sha512-DilSRfPQR38picjx7eyyuXNeduD7hcW/PjT9DZrjXxfLrAQtd17CXJs7HtJevl1wErh/CCSvZlHhjp1++O6GAg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.1.tgz",
|
||||
"integrity": "sha512-SLwYPQg1NJWytq2sd4MnWFmRVGgzwbohBedB2kH0ALsvdnoRYqgjR5HqAsKgoRJO/pphQhHlk3L1gLW62r6hqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-loader": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-loader": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -665,17 +708,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-selection": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.0.tgz",
|
||||
"integrity": "sha512-1PEtreNofysaLxZvgO2CSNCxXhevjYnBdu4IHTFeJKXoq3ckKwkX8fJjyyN4D6+6uXZsnFkHhewl1yKCfKWAWw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.1.tgz",
|
||||
"integrity": "sha512-yef2XB/zR7zjyeUB3Ul0SbTcXqu5isR0GtINkFwL7bJMok6HpYNDnMXSuo55BaxI0dOCnnCSZfoRkAgosnZ1uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -683,16 +726,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-spread": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.0.tgz",
|
||||
"integrity": "sha512-oRLimcod8RhdknN94CQeG+0QndQeiZKIhFUCXDIGxN1Z/qvspZCUty2TC+1kc3G318nZi55pWWphq9sB7ZpqEw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.1.tgz",
|
||||
"integrity": "sha512-RJ/kgJsFRdtWlPMXTW1feUSb6WHIvxtNRLgqzX8dlFIoyc4oZex2Vw+URo/VZuWSe/NvCIihQ20rkNAQJMnNMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-loader": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-loader": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -700,34 +743,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.0.tgz",
|
||||
"integrity": "sha512-w2wzL7m6/sUF54sMVEi8Y8+7VE3BcZqI8THDqobkEkno4Dgmb77FHNPFD6YtAhaRmIoyPnlZf05RDd6Z8ohhkQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.1.tgz",
|
||||
"integrity": "sha512-xv96ESa7JgD5z+TzcOK18/u0gq3d9v7QPv2wpr0ZhcnwLwf4sH0eUJZIsv7z7DMOpBNz7o7jJbrtxDUdCEHGhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-render": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-render": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-tiling": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.0.tgz",
|
||||
"integrity": "sha512-huYi4BJa9KSfqC424bEHw72KBLCR2rfApMeKnpUzAFSdWA6MSYmVBSk8ghnU7XbcLuL6fFBarNsziNrSSnVWTw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.1.tgz",
|
||||
"integrity": "sha512-Q8RF80fb6y9GDAKwvgsu0BsWJlQuhNCtSKWwp3YcZJtIBFm94DVcg0zTgvDmE9/WNOmn4Z1Edt86usmYauHolw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-render": "1.3.0",
|
||||
"@embedpdf/plugin-scroll": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-render": "1.3.1",
|
||||
"@embedpdf/plugin-scroll": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -735,15 +778,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-viewport": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.0.tgz",
|
||||
"integrity": "sha512-AZ7U8DEgEQ8nK5kdrqtukLl5au9NE3mIlFmloyo6Ddrt2rN/Jw1Lt9dsl6wU20GcFQX+hWsg9uAJboLq6AdOCA==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.1.tgz",
|
||||
"integrity": "sha512-gzosrWL18ZhN175Kxocf/p7uqYBhNHvEuV1CpJQmN7ys48aew6Qq8z7MjAsCnJBANXk/8syNdo3qWwBriyjQNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -751,19 +794,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-zoom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.0.tgz",
|
||||
"integrity": "sha512-1VA9aFxoP+BoEpwlR0//jtlD9ESS8nhU8OGGHBRu7IgoWzIx4GqOHgpgXVxzFl9IaLOv69E9DVmwe/yaC6F+0g==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.1.tgz",
|
||||
"integrity": "sha512-3GXpgv6XmZiQnjaPbsxblTqUn84ALFiyONh2gwrEU9apB6STT3TQiY0QRindwrUXdQLpCSjRSB9PpDBCtTww7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0",
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"hammerjs": "^2.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-scroll": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-scroll": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.1.tgz",
|
||||
"integrity": "sha512-6trYysnggwCCTB2q7cX6tkOTbZJNtt2YYZohPCmh0yaDpkfNSgwDwD0jCLtEU2UZLQoH4+2GvNo+4xe+KAGlIQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -1710,6 +1765,19 @@
|
||||
"mlly": "^1.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/utils/node_modules/globals": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -1945,6 +2013,28 @@
|
||||
"react": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@maxim_mazurok/gapi.client.discovery-v1": {
|
||||
"version": "0.4.20200806",
|
||||
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.4.20200806.tgz",
|
||||
"integrity": "sha512-Jeo/KZqK39DI6ExXHcJ4lqnn1O/wEqboQ6eQ8WnNpu5eJ7wUnX/C5KazOgs1aRhnIB/dVzDe8wm62nmtkMIoaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/gapi.client": "*",
|
||||
"@types/gapi.client.discovery-v1": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@maxim_mazurok/gapi.client.drive-v3": {
|
||||
"version": "0.1.20250930",
|
||||
"resolved": "https://registry.npmjs.org/@maxim_mazurok/gapi.client.drive-v3/-/gapi.client.drive-v3-0.1.20250930.tgz",
|
||||
"integrity": "sha512-zNR7HtaFl2Pvf8Ck2zP8cppUst7ouY2isKn7hrGf6hQ4/0ULsu19qMRSQgRb0HxBYcGjak7kGK4pZI4a2z4CWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/gapi.client": "*",
|
||||
"@types/gapi.client.discovery-v1": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.2.tgz",
|
||||
@@ -3531,6 +3621,54 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/gapi": {
|
||||
"version": "0.0.47",
|
||||
"resolved": "https://registry.npmjs.org/@types/gapi/-/gapi-0.0.47.tgz",
|
||||
"integrity": "sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/gapi.client": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/gapi.client/-/gapi.client-1.0.8.tgz",
|
||||
"integrity": "sha512-qJQUmmumbYym3Amax0S8CVzuSngcXsC1fJdwRS2zeW5lM63zXkw4wJFP+bG0jzgi0R6EsJKoHnGNVTDbOyG1ng==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/gapi.client.discovery-v1": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/gapi.client.discovery-v1/-/gapi.client.discovery-v1-0.0.4.tgz",
|
||||
"integrity": "sha512-uevhRumNE65F5mf2gABLaReOmbFSXONuzFZjNR3dYv6BmkHg+wciubHrfBAsp3554zNo3Dcg6dUAlwMqQfpwjQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@maxim_mazurok/gapi.client.discovery-v1": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/gapi.client.drive-v3": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/gapi.client.drive-v3/-/gapi.client.drive-v3-0.0.5.tgz",
|
||||
"integrity": "sha512-yYBxiqMqJVBg4bns4Q28+f2XdJnd3tVA9dxQX1lXMVmzT2B+pZdyCi1u9HLwGveVlookSsAXuqfLfS9KO6MF6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@maxim_mazurok/gapi.client.drive-v3": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/google.accounts": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/google.accounts/-/google.accounts-0.0.18.tgz",
|
||||
"integrity": "sha512-yHaPznll97ZnMJlPABHyeiIlLn3u6gQaUjA5k/O9lrrpgFB9VT10CKPLuKM0qTHMl50uXpW5sIcG+utm8jMOHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/google.picker": {
|
||||
"version": "0.0.51",
|
||||
"resolved": "https://registry.npmjs.org/@types/google.picker/-/google.picker-0.0.51.tgz",
|
||||
"integrity": "sha512-z6o2J4PQTcXvlW1rtgQx65d5uEF+rMI1hzrnazKQxBONdEuYAr4AeOSH2KZy12WHPmqMX+aWYyfcZ0uktBBhhA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
||||
@@ -6365,10 +6503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||
"dev": true,
|
||||
"version": "16.4.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
|
||||
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
+23
-16
@@ -6,22 +6,24 @@
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@embedpdf/core": "^1.3.0",
|
||||
"@embedpdf/engines": "^1.2.1",
|
||||
"@embedpdf/plugin-export": "^1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||
"@embedpdf/plugin-loader": "^1.3.0",
|
||||
"@embedpdf/plugin-pan": "^1.3.0",
|
||||
"@embedpdf/plugin-render": "^1.3.0",
|
||||
"@embedpdf/plugin-rotate": "^1.3.0",
|
||||
"@embedpdf/plugin-scroll": "^1.3.0",
|
||||
"@embedpdf/plugin-search": "^1.3.0",
|
||||
"@embedpdf/plugin-selection": "^1.3.0",
|
||||
"@embedpdf/plugin-spread": "^1.3.0",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.0",
|
||||
"@embedpdf/plugin-tiling": "^1.3.0",
|
||||
"@embedpdf/plugin-viewport": "^1.3.0",
|
||||
"@embedpdf/plugin-zoom": "^1.3.0",
|
||||
"@embedpdf/core": "^1.3.1",
|
||||
"@embedpdf/engines": "^1.3.1",
|
||||
"@embedpdf/plugin-annotation": "^1.3.1",
|
||||
"@embedpdf/plugin-export": "^1.3.1",
|
||||
"@embedpdf/plugin-history": "^1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.1",
|
||||
"@embedpdf/plugin-loader": "^1.3.1",
|
||||
"@embedpdf/plugin-pan": "^1.3.1",
|
||||
"@embedpdf/plugin-render": "^1.3.1",
|
||||
"@embedpdf/plugin-rotate": "^1.3.1",
|
||||
"@embedpdf/plugin-scroll": "^1.3.1",
|
||||
"@embedpdf/plugin-search": "^1.3.1",
|
||||
"@embedpdf/plugin-selection": "^1.3.1",
|
||||
"@embedpdf/plugin-spread": "^1.3.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.1",
|
||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -35,6 +37,7 @@
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"axios": "^1.12.2",
|
||||
"globals": "^16.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
@@ -102,6 +105,10 @@
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/gapi": "^0.0.47",
|
||||
"@types/gapi.client.drive-v3": "^0.0.5",
|
||||
"@types/google.accounts": "^0.0.18",
|
||||
"@types/google.picker": "^0.0.51",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.13",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,7 +87,10 @@
|
||||
"showStack": "Stack-Trace anzeigen",
|
||||
"copyStack": "Stack-Trace kopieren",
|
||||
"githubSubmit": "GitHub - Ein Ticket einreichen",
|
||||
"discordSubmit": "Discord - Unterstützungsbeitrag einreichen"
|
||||
"discordSubmit": "Discord - Unterstützungsbeitrag einreichen",
|
||||
"dismissAllErrors": "Alle Fehler ausblenden",
|
||||
"encryptedPdfMustRemovePassword": "Diese PDF ist verschlüsselt oder passwortgeschützt. Bitte entsperren Sie sie, bevor Sie in PDF/A konvertieren.",
|
||||
"incorrectPasswordProvided": "Das PDF-Passwort ist falsch oder wurde nicht angegeben."
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "Warnung"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "Sortieren nach:",
|
||||
"multiTool": {
|
||||
"title": "PDF-Multitool",
|
||||
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen"
|
||||
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen",
|
||||
"tags": "mehrere,werkzeuge"
|
||||
},
|
||||
"merge": {
|
||||
"title": "Zusammenführen",
|
||||
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen"
|
||||
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen",
|
||||
"tags": "kombinieren,zusammenführen,vereinen"
|
||||
},
|
||||
"split": {
|
||||
"title": "Aufteilen",
|
||||
"desc": "PDFs in mehrere Dokumente aufteilen"
|
||||
"desc": "PDFs in mehrere Dokumente aufteilen",
|
||||
"tags": "teilen,trennen,aufteilen"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "Drehen",
|
||||
"desc": "Drehen Sie Ihre PDFs ganz einfach"
|
||||
"desc": "Drehen Sie Ihre PDFs ganz einfach",
|
||||
"tags": "drehen,spiegeln,ausrichten"
|
||||
},
|
||||
"convert": {
|
||||
"title": "Umwandeln",
|
||||
"desc": "Dateien zwischen verschiedenen Formaten konvertieren"
|
||||
"desc": "Dateien zwischen verschiedenen Formaten konvertieren",
|
||||
"tags": "umwandeln,ändern"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "Organisieren",
|
||||
"desc": "Seiten entfernen und Seitenreihenfolge ändern"
|
||||
"desc": "Seiten entfernen und Seitenreihenfolge ändern",
|
||||
"tags": "organisieren,umordnen,neu anordnen"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "Bild einfügen",
|
||||
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)"
|
||||
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)",
|
||||
"tags": "einfügen,einbetten,platzieren"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "Anhänge hinzufügen",
|
||||
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen"
|
||||
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen",
|
||||
"tags": "einbetten,anhängen,einfügen"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Wasserzeichen hinzufügen",
|
||||
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu"
|
||||
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu",
|
||||
"tags": "stempel,markierung,überlagerung"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "Passwort entfernen",
|
||||
"desc": "Den Passwortschutz eines PDFs entfernen"
|
||||
"desc": "Den Passwortschutz eines PDFs entfernen",
|
||||
"tags": "entsperren"
|
||||
},
|
||||
"compress": {
|
||||
"title": "Komprimieren",
|
||||
"desc": "PDF komprimieren um die Dateigröße zu reduzieren"
|
||||
"desc": "PDF komprimieren um die Dateigröße zu reduzieren",
|
||||
"tags": "verkleinern,reduzieren,optimieren"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Schreibgeschützte PDF-Formfelder entfernen",
|
||||
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument."
|
||||
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument.",
|
||||
"tags": "entsperren,aktivieren,bearbeiten"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "Metadaten ändern",
|
||||
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument"
|
||||
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument",
|
||||
"tags": "bearbeiten,ändern,aktualisieren"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "Führe OCR/Cleanup-Scans aus",
|
||||
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu"
|
||||
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu",
|
||||
"tags": "extrahieren,scannen"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "Bilder extrahieren",
|
||||
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv"
|
||||
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv",
|
||||
"tags": "extrahieren,speichern,exportieren"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "Gescannte Fotos erkennen/aufteilen",
|
||||
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf"
|
||||
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf",
|
||||
"tags": "erkennen,teilen,fotos"
|
||||
},
|
||||
"sign": {
|
||||
"title": "Signieren",
|
||||
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu"
|
||||
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu",
|
||||
"tags": "unterschrift,autogramm"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Abflachen",
|
||||
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen"
|
||||
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen",
|
||||
"tags": "vereinfachen,entfernen,interaktiv"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "Mit Zertifikat signieren",
|
||||
"desc": "Ein PDF mit einem Zertifikat/Schlüssel (PEM/P12) signieren"
|
||||
"desc": "Ein PDF mit einem Zertifikat/Schlüssel (PEM/P12) signieren",
|
||||
"tags": "authentifizieren,PEM,P12,offiziell,verschlüsseln,signieren,zertifikat,PKCS12,JKS,server,manuell,auto"
|
||||
},
|
||||
"repair": {
|
||||
"title": "Reparatur",
|
||||
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren"
|
||||
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren",
|
||||
"tags": "reparieren,wiederherstellen"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "Leere Seiten entfernen",
|
||||
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument"
|
||||
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument",
|
||||
"tags": "löschen,bereinigen,leer"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "Anmerkungen entfernen",
|
||||
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF"
|
||||
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF",
|
||||
"tags": "löschen,bereinigen,entfernen"
|
||||
},
|
||||
"compare": {
|
||||
"title": "Vergleichen",
|
||||
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an"
|
||||
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an",
|
||||
"tags": "unterschied"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "Zertifikatsignatur entfernen",
|
||||
"desc": "Zertifikatsignatur aus PDF entfernen"
|
||||
"desc": "Zertifikatsignatur aus PDF entfernen",
|
||||
"tags": "entfernen,löschen,entsperren"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "Mehrseitiges Layout",
|
||||
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen"
|
||||
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen",
|
||||
"tags": "layout,anordnen,kombinieren"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Broschüren-Layout",
|
||||
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen"
|
||||
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen",
|
||||
"tags": "broschüre,druck,bindung"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Seitengröße/Skalierung anpassen",
|
||||
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern"
|
||||
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern",
|
||||
"tags": "größe ändern,anpassen,skalieren"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "Seitenzahlen hinzufügen",
|
||||
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle"
|
||||
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle",
|
||||
"tags": "nummerieren,paginierung,zählen"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "PDF-Datei automatisch umbenennen",
|
||||
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um"
|
||||
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um",
|
||||
"tags": "auto-erkennung,kopfzeilen-basiert,organisieren,umbenennen"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Farben/Kontrast anpassen",
|
||||
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen"
|
||||
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen",
|
||||
"tags": "kontrast,helligkeit,sättigung"
|
||||
},
|
||||
"crop": {
|
||||
"title": "PDF zuschneiden",
|
||||
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)"
|
||||
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)",
|
||||
"tags": "zuschneiden,schneiden,größe ändern"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "PDF automatisch teilen",
|
||||
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen"
|
||||
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen",
|
||||
"tags": "auto,teilen,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Bereinigen",
|
||||
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen"
|
||||
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen",
|
||||
"tags": "bereinigen,löschen,entfernen"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "Alle Informationen anzeigen",
|
||||
"desc": "Erfasst alle möglichen Informationen in einer PDF"
|
||||
"desc": "Erfasst alle möglichen Informationen in einer PDF",
|
||||
"tags": "info,metadaten,details"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF zu einer Seite zusammenfassen",
|
||||
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen"
|
||||
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen",
|
||||
"tags": "kombinieren,zusammenführen,einzeln"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "Javascript anzeigen",
|
||||
"desc": "Alle Javascript Funktionen in einer PDF anzeigen"
|
||||
"desc": "Alle Javascript Funktionen in einer PDF anzeigen",
|
||||
"tags": "javascript,code,skript"
|
||||
},
|
||||
"redact": {
|
||||
"title": "Manuell zensieren/schwärzen",
|
||||
"desc": "Zensiere (Schwärze) eine PDF-Datei durch Auswählen von Text, gezeichneten Formen und/oder ausgewählten Seite(n)"
|
||||
"desc": "Zensiere (Schwärze) eine PDF-Datei durch Auswählen von Text, gezeichneten Formen und/oder ausgewählten Seite(n)",
|
||||
"tags": "zensieren,schwärzen,verbergen"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "PDFs überlagern",
|
||||
"desc": "PDFs über eine andere PDF überlagern"
|
||||
"desc": "PDFs über eine andere PDF überlagern",
|
||||
"tags": "überlagern,kombinieren,stapeln"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "PDF nach Abschnitten aufteilen",
|
||||
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen"
|
||||
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen",
|
||||
"tags": "teilen,abschnitte,aufteilen"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "Stempel zu PDF hinzufügen",
|
||||
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen"
|
||||
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen",
|
||||
"tags": "stempel,markierung,siegel"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "Bild entfernen",
|
||||
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern"
|
||||
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern",
|
||||
"tags": "entfernen,löschen,bereinigen"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "PDF-Datei nach Kapiteln aufteilen",
|
||||
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur."
|
||||
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.",
|
||||
"tags": "teilen,kapitel,struktur"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "PDF-Signatur überprüfen",
|
||||
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen"
|
||||
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen",
|
||||
"tags": "validieren,überprüfen,zertifikat"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API-Dokumentation",
|
||||
"desc": "API-Dokumentation anzeigen und Endpunkte testen"
|
||||
"desc": "API-Dokumentation anzeigen und Endpunkte testen",
|
||||
"tags": "API,dokumentation,test"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Scan simulieren",
|
||||
@@ -538,42 +585,52 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Inhaltsverzeichnis bearbeiten",
|
||||
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten"
|
||||
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten",
|
||||
"tags": "lesezeichen,inhalt,bearbeiten"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Zertifikate verwalten",
|
||||
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen."
|
||||
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen.",
|
||||
"tags": "zertifikate,importieren,exportieren"
|
||||
},
|
||||
"read": {
|
||||
"title": "Lesen",
|
||||
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen."
|
||||
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen.",
|
||||
"tags": "anzeigen,öffnen,anzeigen"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Seiten neu anordnen",
|
||||
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen."
|
||||
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen.",
|
||||
"tags": "umordnen,neu anordnen,organisieren"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "Seiten extrahieren",
|
||||
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren"
|
||||
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren",
|
||||
"tags": "extrahieren,auswählen,kopieren"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "Entfernen",
|
||||
"desc": "Ungewollte Seiten aus dem PDF entfernen"
|
||||
"desc": "Ungewollte Seiten aus dem PDF entfernen",
|
||||
"tags": "löschen,extrahieren,ausschließen"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "Teilen nach Größe/Anzahl",
|
||||
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf"
|
||||
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf",
|
||||
"tags": "auto,teilen,größe"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Farbe ersetzen und invertieren",
|
||||
"desc": "Ersetzen Sie die Farbe des Texts und Hintergrund der PDF-Datei und invertieren Sie die komplette Farbe der PDF-Datei, um die Dateigröße zu reduzieren"
|
||||
},
|
||||
"devApi": {
|
||||
"desc": "Link zur API-Dokumentation"
|
||||
"desc": "Link zur API-Dokumentation",
|
||||
"tags": "API,entwicklung,dokumentation",
|
||||
"title": "API"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "Automatische Ordnerüberwachung",
|
||||
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen"
|
||||
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen",
|
||||
"tags": "automatisierung,ordner,scannen"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "SSO-Anleitung",
|
||||
@@ -593,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automatisieren",
|
||||
"desc": "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben."
|
||||
"desc": "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben.",
|
||||
"tags": "arbeitsablauf,sequenz,automatisierung"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "Farben in PDF-Dokumenten ersetzen oder invertieren",
|
||||
"title": "Farbe ersetzen & invertieren"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "Erstellen Sie eine PDF, die aussieht, als wäre sie gescannt worden",
|
||||
"tags": "scannen,simulieren,erstellen",
|
||||
"title": "Scanner-Effekt"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -633,8 +700,18 @@
|
||||
"merge": {
|
||||
"tags": "zusammenführen,seitenvorgänge,back end,serverseitig",
|
||||
"title": "Zusammenführen",
|
||||
"removeDigitalSignature": "Digitale Signatur in der zusammengeführten Datei entfernen?",
|
||||
"generateTableOfContents": "Inhaltsverzeichnis in der zusammengeführten Datei erstellen?",
|
||||
"removeDigitalSignature": {
|
||||
"tooltip": {
|
||||
"description": "Digitale Signaturen werden beim Zusammenführen von Dateien ungültig. Aktivieren Sie diese Option, um sie aus der endgültigen zusammengeführten PDF zu entfernen.",
|
||||
"title": "Digitale Signatur entfernen"
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": {
|
||||
"tooltip": {
|
||||
"description": "Erstellt automatisch ein klickbares Inhaltsverzeichnis in der zusammengeführten PDF basierend auf den ursprünglichen Dateinamen und Seitenzahlen.",
|
||||
"title": "Inhaltsverzeichnis generieren"
|
||||
}
|
||||
},
|
||||
"submit": "Zusammenführen",
|
||||
"sortBy": {
|
||||
"description": "Dateien werden in der Reihenfolge zusammengeführt, in der sie ausgewählt wurden. Ziehen Sie zum Neuordnen oder sortieren Sie unten.",
|
||||
@@ -860,7 +937,13 @@
|
||||
"images": "Bilder",
|
||||
"officeDocs": "Office-Dokumente (Word, Excel, PowerPoint)",
|
||||
"imagesExt": "Bilder (JPG, PNG, usw.)",
|
||||
"grayscale": "Graustufen"
|
||||
"grayscale": "Graustufen",
|
||||
"dpi": "DPI",
|
||||
"markdown": "Markdown",
|
||||
"odtExt": "OpenDocument Text (.odt)",
|
||||
"pptExt": "PowerPoint (.pptx)",
|
||||
"rtfExt": "Rich Text Format (.rtf)",
|
||||
"textRtf": "Text/RTF"
|
||||
},
|
||||
"imageToPdf": {
|
||||
"tags": "konvertierung,img,jpg,bild,foto"
|
||||
@@ -900,7 +983,20 @@
|
||||
"10": "Ungerade-Gerade-Zusammenführung",
|
||||
"11": "Alle Seiten duplizieren"
|
||||
},
|
||||
"placeholder": "(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)"
|
||||
"placeholder": "(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "Seiten für den Broschüren-Druck anordnen (letzte, erste, zweite, vorletzte, …).",
|
||||
"CUSTOM": "Verwenden Sie eine benutzerdefinierte Sequenz von Seitenzahlen oder Ausdrücken, um eine neue Reihenfolge zu definieren.",
|
||||
"DUPLEX_SORT": "Vorder- und Rückseiten verschachteln, als ob ein Duplex-Scanner alle Vorderseiten und dann alle Rückseiten gescannt hätte (1, n, 2, n-1, …).",
|
||||
"DUPLICATE": "Jede Seite entsprechend der benutzerdefinierten Anzahl duplizieren (z.B. 4 dupliziert jede Seite 4×).",
|
||||
"ODD_EVEN_MERGE": "Zwei PDFs durch abwechselnde Seiten zusammenführen: ungerade aus der ersten, gerade aus der zweiten.",
|
||||
"ODD_EVEN_SPLIT": "Das Dokument in zwei Ausgaben aufteilen: alle ungeraden Seiten und alle geraden Seiten.",
|
||||
"REMOVE_FIRST": "Die erste Seite aus dem Dokument entfernen.",
|
||||
"REMOVE_FIRST_AND_LAST": "Sowohl die erste als auch die letzte Seite aus dem Dokument entfernen.",
|
||||
"REMOVE_LAST": "Die letzte Seite aus dem Dokument entfernen.",
|
||||
"REVERSE_ORDER": "Das Dokument umkehren, sodass die letzte Seite zur ersten wird usw.",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "Seiten für den Seitenheft-Broschüren-Druck anordnen (optimiert für die Bindung an der Seite)."
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "img,jpg,bild,foto",
|
||||
@@ -929,7 +1025,8 @@
|
||||
"failed": "Ein Fehler ist beim Hinzufügen des Wasserzeichens zur PDF aufgetreten."
|
||||
},
|
||||
"watermarkType": {
|
||||
"image": "Bild"
|
||||
"image": "Bild",
|
||||
"text": "Text"
|
||||
},
|
||||
"settings": {
|
||||
"type": "Wasserzeichen-Typ",
|
||||
@@ -1333,7 +1430,9 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "Trapped-Status",
|
||||
"unknown": "Unbekannt"
|
||||
"unknown": "Unbekannt",
|
||||
"false": "Falsch",
|
||||
"true": "Wahr"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweiterte Optionen"
|
||||
@@ -1522,7 +1621,13 @@
|
||||
"header": "Bilder extrahieren",
|
||||
"selectText": "Wählen Sie das Bildformat aus, in das extrahierte Bilder konvertiert werden sollen",
|
||||
"allowDuplicates": "Doppelte Bilder speichern",
|
||||
"submit": "Extrahieren"
|
||||
"submit": "Extrahieren",
|
||||
"error": {
|
||||
"failed": "Beim Extrahieren der Bilder aus der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "archiv,langfristig,standard,konvertierung,speicherung,aufbewahrung",
|
||||
@@ -1599,8 +1704,14 @@
|
||||
"title": "Signieren",
|
||||
"header": "PDFs signieren",
|
||||
"upload": "Bild hochladen",
|
||||
"draw": "Signatur zeichnen",
|
||||
"text": "Texteingabe",
|
||||
"draw": {
|
||||
"clear": "Löschen",
|
||||
"title": "Zeichnen Sie Ihre Unterschrift"
|
||||
},
|
||||
"text": {
|
||||
"name": "Name des Unterzeichners",
|
||||
"placeholder": "Geben Sie Ihren vollständigen Namen ein"
|
||||
},
|
||||
"clear": "Leeren",
|
||||
"add": "Signieren",
|
||||
"saved": "Gespeicherte Signaturen",
|
||||
@@ -1616,7 +1727,35 @@
|
||||
"previous": "Vorherige Seite",
|
||||
"maintainRatio": "Seitenverhältnis beibehalten ein-/ausschalten",
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederherstellen"
|
||||
"redo": "Wiederherstellen",
|
||||
"activate": "Signatur-Platzierung aktivieren",
|
||||
"applySignatures": "Signaturen anwenden",
|
||||
"deactivate": "Signatur-Platzierung beenden",
|
||||
"error": {
|
||||
"failed": "Beim Signieren der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"image": {
|
||||
"hint": "Laden Sie ein PNG- oder JPG-Bild Ihrer Unterschrift hoch",
|
||||
"label": "Unterschriftsbild hochladen",
|
||||
"placeholder": "Bilddatei auswählen"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "So fügen Sie eine Unterschrift hinzu"
|
||||
},
|
||||
"results": {
|
||||
"title": "Signatur-Ergebnisse"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "Signatur konfigurieren"
|
||||
},
|
||||
"submit": "Dokument signieren",
|
||||
"type": {
|
||||
"canvas": "Canvas",
|
||||
"draw": "Zeichnen",
|
||||
"image": "Bild",
|
||||
"text": "Text",
|
||||
"title": "Signaturtyp"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "statisch,deaktivieren,nicht interaktiv,optimieren",
|
||||
@@ -1635,7 +1774,8 @@
|
||||
"stepTitle": "Abflachungs-Optionen",
|
||||
"title": "Abflachungs-Optionen",
|
||||
"flattenOnlyForms.desc": "Nur Formularfelder vereinfachen, andere interaktive Elemente unverändert lassen",
|
||||
"note": "Das Abflachen entfernt interaktive Elemente aus der PDF und macht sie nicht mehr bearbeitbar."
|
||||
"note": "Das Abflachen entfernt interaktive Elemente aus der PDF und macht sie nicht mehr bearbeitbar.",
|
||||
"flattenOnlyForms": "Nur Formulare vereinfachen"
|
||||
},
|
||||
"results": {
|
||||
"title": "Reduzierungs-Ergebnisse"
|
||||
@@ -1693,7 +1833,8 @@
|
||||
"label": "Pixel-Weißheitsschwellwert"
|
||||
},
|
||||
"whitePercent": {
|
||||
"label": "Weiß-Prozentsatz-Schwellwert"
|
||||
"label": "Weiß-Prozentsatz-Schwellwert",
|
||||
"unit": "%"
|
||||
},
|
||||
"includeBlankPages": {
|
||||
"label": "Erkannte leere Seiten einschließen"
|
||||
@@ -1730,7 +1871,17 @@
|
||||
"tags": "kommentare,hervorheben,notizen,markieren,entfernen",
|
||||
"title": "Kommentare entfernen",
|
||||
"header": "Kommentare entfernen",
|
||||
"submit": "Entfernen"
|
||||
"submit": "Entfernen",
|
||||
"error": {
|
||||
"failed": "Beim Entfernen der Anmerkungen aus der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"info": {
|
||||
"description": "Dieses Werkzeug entfernt alle Anmerkungen (Kommentare, Hervorhebungen, Notizen usw.) aus Ihren PDF-Dokumenten.",
|
||||
"title": "Über Anmerkungen entfernen"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differenzieren,kontrastieren,verändern,analysieren",
|
||||
@@ -2015,7 +2166,9 @@
|
||||
},
|
||||
"pageSize": {
|
||||
"label": "Ziel-Seitengröße",
|
||||
"keep": "Ursprüngliche Größe beibehalten"
|
||||
"keep": "Ursprüngliche Größe beibehalten",
|
||||
"legal": "Legal",
|
||||
"letter": "Letter"
|
||||
},
|
||||
"submit": "Seitenskalierung anpassen",
|
||||
"error": {
|
||||
@@ -2306,7 +2459,8 @@
|
||||
"showLayers": "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzustand zurückzusetzen)",
|
||||
"colourPicker": "Farbwähler",
|
||||
"findCurrentOutlineItem": "Aktuelles Gliederungselement finden",
|
||||
"applyChanges": "Änderungen anwenden"
|
||||
"applyChanges": "Änderungen anwenden",
|
||||
"zoom": "Zoom"
|
||||
}
|
||||
},
|
||||
"tableExtraxt": {
|
||||
@@ -2496,7 +2650,8 @@
|
||||
"magicLinkSent": "Magic Link wurde an {{email}} gesendet! Prüfen Sie Ihre E-Mails und klicken Sie auf den Link zur Anmeldung.",
|
||||
"passwordResetSent": "Passwort-Reset-Link wurde an {{email}} gesendet! Prüfen Sie Ihre E-Mails und folgen Sie den Anweisungen.",
|
||||
"failedToSignIn": "Anmeldung mit {{provider}} fehlgeschlagen: {{message}}",
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}"
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}",
|
||||
"debug": "Debug"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Konto erstellen",
|
||||
@@ -2518,7 +2673,8 @@
|
||||
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||
"checkEmailConfirmation": "Prüfen Sie Ihre E-Mails auf einen Bestätigungslink, um die Registrierung abzuschließen.",
|
||||
"accountCreatedSuccessfully": "Konto erfolgreich erstellt! Sie können sich jetzt anmelden.",
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}"
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}",
|
||||
"name": "Name"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF zu einer Seite zusammenfassen",
|
||||
@@ -2961,7 +3117,12 @@
|
||||
"selectedCount": "{{count}} ausgewählt",
|
||||
"download": "Herunterladen",
|
||||
"delete": "Löschen",
|
||||
"unsupported": "Nicht unterstützt"
|
||||
"unsupported": "Nicht unterstützt",
|
||||
"fileFormat": "Format",
|
||||
"fileName": "Name",
|
||||
"fileVersion": "Version",
|
||||
"googleDrive": "Google Drive",
|
||||
"googleDriveShort": "Drive"
|
||||
},
|
||||
"storage": {
|
||||
"temporaryNotice": "Dateien werden temporär in Ihrem Browser gespeichert und können automatisch gelöscht werden",
|
||||
@@ -2992,12 +3153,24 @@
|
||||
"options": {
|
||||
"title": "Bereinigungs-Optionen",
|
||||
"note": "Wählen Sie die Elemente aus, die Sie aus der PDF entfernen möchten. Mindestens eine Option muss ausgewählt werden.",
|
||||
"removeJavaScript": "JavaScript entfernen",
|
||||
"removeEmbeddedFiles": "Eingebettete Dateien entfernen",
|
||||
"removeXMPMetadata": "XMP-Metadaten entfernen",
|
||||
"removeMetadata": "Dokument-Metadaten entfernen",
|
||||
"removeLinks": "Links entfernen",
|
||||
"removeFonts": "Schriftarten entfernen"
|
||||
"removeJavaScript": {
|
||||
"desc": "JavaScript-Aktionen und Skripte aus der PDF entfernen"
|
||||
},
|
||||
"removeEmbeddedFiles": {
|
||||
"desc": "Alle in der PDF eingebetteten Dateien entfernen"
|
||||
},
|
||||
"removeXMPMetadata": {
|
||||
"desc": "XMP-Metadaten aus der PDF entfernen"
|
||||
},
|
||||
"removeMetadata": {
|
||||
"desc": "Dokumentinformations-Metadaten (Titel, Autor usw.) entfernen"
|
||||
},
|
||||
"removeLinks": {
|
||||
"desc": "Externe Links und Launch-Aktionen aus der PDF entfernen"
|
||||
},
|
||||
"removeFonts": {
|
||||
"desc": "Eingebettete Schriftarten aus der PDF entfernen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3025,7 +3198,8 @@
|
||||
"keyLength": {
|
||||
"label": "Verschlüsselungsschlüssellänge",
|
||||
"40bit": "40-bit (Niedrig)",
|
||||
"256bit": "256-bit (Hoch)"
|
||||
"256bit": "256-bit (Hoch)",
|
||||
"128bit": "128-bit (Standard)"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
@@ -3264,5 +3438,58 @@
|
||||
},
|
||||
"generateError": "Wir konnten Ihren API-Schlüssel nicht generieren."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "Weitere Dateien hinzufügen...",
|
||||
"attachments": "Anhänge auswählen",
|
||||
"info": "Wählen Sie Dateien aus, die Sie Ihrer PDF anhängen möchten. Diese Dateien werden eingebettet und über das Anhangs-Panel der PDF zugänglich sein.",
|
||||
"placeholder": "Dateien auswählen...",
|
||||
"results": {
|
||||
"title": "Anhangs-Ergebnisse"
|
||||
},
|
||||
"selectFiles": "Dateien zum Anhängen auswählen",
|
||||
"selectedFiles": "Ausgewählte Dateien",
|
||||
"submit": "Anhänge hinzufügen"
|
||||
},
|
||||
"applyAndContinue": "Anwenden & Fortfahren",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
"exportAndContinue": "Exportieren & Fortfahren",
|
||||
"keepWorking": "Weiterarbeiten",
|
||||
"logOut": "Abmelden",
|
||||
"replaceColor": {
|
||||
"tags": "Farbe ersetzen,Seitenoperationen,Backend,serverseitig"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "Beim Extrahieren der Bild-Scans ist ein Fehler aufgetreten."
|
||||
},
|
||||
"submit": "Bild-Scans extrahieren",
|
||||
"title": "Extrahierte Bilder",
|
||||
"tooltip": {
|
||||
"headsUp": "Hinweis",
|
||||
"headsUpDesc": "Überlappende Fotos oder Hintergründe, die farblich sehr nah an den Fotos liegen, können die Genauigkeit verringern - versuchen Sie einen helleren oder dunkleren Hintergrund und lassen Sie mehr Platz.",
|
||||
"problem1": "Fotos nicht erkannt → Toleranz auf 30-50 erhöhen",
|
||||
"problem2": "Zu viele Falscherkennungen → Mindestfläche auf 15.000-20.000 erhöhen",
|
||||
"problem3": "Zuschnitte sind zu eng → Randgröße auf 5-10 erhöhen",
|
||||
"problem4": "Geneigte Fotos nicht begradigt → Winkelschwelle auf ~5° senken",
|
||||
"problem5": "Staub-/Rausch-Boxen → Mindest-Konturfläche auf 1000-2000 erhöhen",
|
||||
"quickFixes": "Schnelle Lösungen",
|
||||
"setupTips": "Einrichtungstipps",
|
||||
"tip1": "Verwenden Sie einen einfachen, hellen Hintergrund",
|
||||
"tip2": "Lassen Sie einen kleinen Abstand (≈1 cm) zwischen den Fotos",
|
||||
"tip3": "Scannen Sie mit 300-600 DPI",
|
||||
"tip4": "Reinigen Sie die Scanner-Glasplatte",
|
||||
"title": "Foto-Teiler",
|
||||
"useCase1": "Ganze Album-Seiten in einem Durchgang scannen",
|
||||
"useCase2": "Flachbett-Stapel in separate Dateien aufteilen",
|
||||
"useCase3": "Collagen in einzelne Fotos aufteilen",
|
||||
"useCase4": "Fotos aus Dokumenten extrahieren",
|
||||
"whatThisDoes": "Was dies tut",
|
||||
"whatThisDoesDesc": "Findet und extrahiert automatisch jedes Foto von einer gescannten Seite oder einem zusammengesetzten Bild - kein manuelles Zuschneiden erforderlich.",
|
||||
"whenToUse": "Wann zu verwenden"
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "Allgemeine Geschäftsbedingungen",
|
||||
"unsavedChanges": "Sie haben ungespeicherte Änderungen an Ihrer PDF. Was möchten Sie tun?",
|
||||
"unsavedChangesTitle": "Ungespeicherte Änderungen"
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"unsavedChanges": "You have unsaved changes to your PDF.",
|
||||
"areYouSure": "Are you sure you want to leave?",
|
||||
"unsavedChangesTitle": "Unsaved Changes",
|
||||
"keepWorking": "Keep Working",
|
||||
"discardChanges": "Discard & Leave",
|
||||
"applyAndContinue": "Save & Leave",
|
||||
"exportAndContinue": "Export & Continue",
|
||||
"language": {
|
||||
"direction": "ltr"
|
||||
},
|
||||
@@ -10,16 +17,34 @@
|
||||
"selectText": {
|
||||
"1": "Select PDF file:",
|
||||
"2": "Margin Size",
|
||||
"3": "Position",
|
||||
"3": "Position Selection",
|
||||
"4": "Starting Number",
|
||||
"5": "Pages to Number",
|
||||
"6": "Custom Text"
|
||||
"6": "Custom Text Format"
|
||||
},
|
||||
"customTextDesc": "Custom Text",
|
||||
"numberPagesDesc": "Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc",
|
||||
"customNumberDesc": "Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}",
|
||||
"submit": "Add Page Numbers"
|
||||
"numberPagesDesc": "e.g., 1,3,5-8 or leave blank for all pages",
|
||||
"customNumberDesc": "e.g., \"Page {n}\" or leave blank for just numbers",
|
||||
"submit": "Add Page Numbers",
|
||||
"configuration": "Configuration",
|
||||
"customize": "Customize Appearance",
|
||||
"pagesAndStarting": "Pages & Starting Number",
|
||||
"positionAndPages": "Position & Pages",
|
||||
"error": {
|
||||
"failed": "Add page numbers operation failed"
|
||||
},
|
||||
"results": {
|
||||
"title": "Page Number Results"
|
||||
},
|
||||
"preview": "Position Selection",
|
||||
"previewDisclaimer": "Preview is approximate. Final output may vary due to PDF font metrics."
|
||||
},
|
||||
"pageSelectionPrompt": "Specify which pages to add numbers to. Examples: \"1,3,5\" for specific pages, \"1-5\" for ranges, \"2n\" for even pages, or leave blank for all pages.",
|
||||
"startingNumberTooltip": "The first number to display. Subsequent pages will increment from this number.",
|
||||
"marginTooltip": "Distance between the page number and the edge of the page.",
|
||||
"fontSizeTooltip": "Size of the page number text in points. Larger numbers create bigger text.",
|
||||
"fontTypeTooltip": "Font family for the page numbers. Choose based on your document style.",
|
||||
"customTextTooltip": "Optional custom format for page numbers. Use {n} as placeholder for the number. Example: \"Page {n}\" will show \"Page 1\", \"Page 2\", etc.",
|
||||
"pdfPrompt": "Select PDF(s)",
|
||||
"multiPdfPrompt": "Select PDFs (2+)",
|
||||
"multiPdfDropPrompt": "Select (or drag & drop) all PDFs you require",
|
||||
@@ -41,6 +66,8 @@
|
||||
"save": "Save",
|
||||
"saveToBrowser": "Save to Browser",
|
||||
"download": "Download",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
"undoOperationTooltip": "Click to undo the last operation and restore the original files",
|
||||
"undo": "Undo",
|
||||
"moreOptions": "More Options",
|
||||
@@ -231,6 +258,33 @@
|
||||
"cacheInputs": {
|
||||
"name": "Save form inputs",
|
||||
"help": "Enable to store previously used inputs for future runs"
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"description": "Configure general application preferences.",
|
||||
"autoUnzip": "Auto-unzip API responses",
|
||||
"autoUnzipDescription": "Automatically extract files from ZIP responses",
|
||||
"autoUnzipTooltip": "Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.",
|
||||
"autoUnzipFileLimit": "Auto-unzip file limit",
|
||||
"autoUnzipFileLimitDescription": "Maximum number of files to extract from ZIP",
|
||||
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs."
|
||||
},
|
||||
"hotkeys": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"description": "Hover a tool to see its shortcut or customise it below. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel.",
|
||||
"errorModifier": {
|
||||
"mac": "Include ⌘ (Command), ⌥ (Option), or another modifier in your shortcut.",
|
||||
"windows": "Include Ctrl, Alt, or another modifier in your shortcut."
|
||||
},
|
||||
"errorConflict": "Shortcut already used by {{tool}}.",
|
||||
"none": "Not assigned",
|
||||
"customBadge": "Custom",
|
||||
"defaultLabel": "Default: {{shortcut}}",
|
||||
"capturing": "Press keys… (Esc to cancel)",
|
||||
"change": "Change shortcut",
|
||||
"reset": "Reset",
|
||||
"shortcut": "Shortcut",
|
||||
"noShortcut": "No shortcut set"
|
||||
}
|
||||
},
|
||||
"changeCreds": {
|
||||
@@ -507,7 +561,7 @@
|
||||
"adjustContrast": {
|
||||
"tags": "contrast,brightness,saturation",
|
||||
"title": "Adjust Colours/Contrast",
|
||||
"desc": "Adjust Contrast, Saturation and Brightness of a PDF"
|
||||
"desc": "Adjust Colors/Contrast, Saturation and Brightness of a PDF"
|
||||
},
|
||||
"crop": {
|
||||
"tags": "trim,cut,resize",
|
||||
@@ -579,9 +633,9 @@
|
||||
"title": "API Documentation",
|
||||
"desc": "View API documentation and test endpoints"
|
||||
},
|
||||
"fakeScan": {
|
||||
"scannerEffect": {
|
||||
"tags": "scan,simulate,create",
|
||||
"title": "Fake Scan",
|
||||
"title": "Scanner Effect",
|
||||
"desc": "Create a PDF that looks like it was scanned"
|
||||
},
|
||||
"editTableOfContents": {
|
||||
@@ -844,6 +898,7 @@
|
||||
"rotate": {
|
||||
"title": "Rotate PDF",
|
||||
"submit": "Apply Rotation",
|
||||
"selectRotation": "Select Rotation Angle (Clockwise)",
|
||||
"error": {
|
||||
"failed": "An error occurred while rotating the PDF."
|
||||
},
|
||||
@@ -1726,6 +1781,7 @@
|
||||
"add": "Add",
|
||||
"saved": "Saved Signatures",
|
||||
"save": "Save Signature",
|
||||
"applySignatures": "Apply Signatures",
|
||||
"personalSigs": "Personal Signatures",
|
||||
"sharedSigs": "Shared Signatures",
|
||||
"noSavedSigs": "No saved signatures found",
|
||||
@@ -1737,7 +1793,42 @@
|
||||
"previous": "Previous page",
|
||||
"maintainRatio": "Toggle maintain aspect ratio",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
"redo": "Redo",
|
||||
"submit": "Sign Document",
|
||||
"steps": {
|
||||
"configure": "Configure Signature"
|
||||
},
|
||||
"type": {
|
||||
"title": "Signature Type",
|
||||
"draw": "Draw",
|
||||
"canvas": "Canvas",
|
||||
"image": "Image",
|
||||
"text": "Text"
|
||||
},
|
||||
"draw": {
|
||||
"title": "Draw your signature",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"image": {
|
||||
"label": "Upload signature image",
|
||||
"placeholder": "Select image file",
|
||||
"hint": "Upload a PNG or JPG image of your signature"
|
||||
},
|
||||
"text": {
|
||||
"name": "Signer Name",
|
||||
"placeholder": "Enter your full name"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "How to add signature"
|
||||
},
|
||||
"activate": "Activate Signature Placement",
|
||||
"deactivate": "Stop Placing Signatures",
|
||||
"results": {
|
||||
"title": "Signature Results"
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while signing the PDF."
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Flatten",
|
||||
@@ -1851,7 +1942,17 @@
|
||||
"tags": "comments,highlight,notes,markup,remove",
|
||||
"title": "Remove Annotations",
|
||||
"header": "Remove Annotations",
|
||||
"submit": "Remove"
|
||||
"submit": "Remove",
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
},
|
||||
"info": {
|
||||
"title": "About Remove Annotations",
|
||||
"description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while removing annotations from the PDF."
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differentiate,contrast,changes,analysis",
|
||||
@@ -2704,8 +2805,9 @@
|
||||
"submit": "Sanitize PDF"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Adjust Contrast",
|
||||
"header": "Adjust Contrast",
|
||||
"title": "Adjust Colors/Contrast",
|
||||
"header": "Adjust Colors/Contrast",
|
||||
"basic": "Basic Adjustments",
|
||||
"contrast": "Contrast:",
|
||||
"brightness": "Brightness:",
|
||||
"saturation": "Saturation:",
|
||||
@@ -2992,7 +3094,12 @@
|
||||
"panMode": "Pan Mode",
|
||||
"rotateLeft": "Rotate Left",
|
||||
"rotateRight": "Rotate Right",
|
||||
"toggleSidebar": "Toggle Sidebar"
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"exportSelected": "Export Selected Pages",
|
||||
"toggleAnnotations": "Toggle Annotations Visibility",
|
||||
"annotationMode": "Toggle Annotation Mode",
|
||||
"draw": "Draw",
|
||||
"save": "Save"
|
||||
},
|
||||
"search": {
|
||||
"title": "Search PDF",
|
||||
@@ -3034,6 +3141,7 @@
|
||||
"automate": "Automate",
|
||||
"files": "Files",
|
||||
"activity": "Activity",
|
||||
"account": "Account",
|
||||
"config": "Config",
|
||||
"allTools": "All Tools"
|
||||
},
|
||||
@@ -3061,6 +3169,9 @@
|
||||
"addFiles": "Add Files",
|
||||
"dragFilesInOrClick": "Drag files in or click \"Add Files\" to browse"
|
||||
},
|
||||
"fileEditor": {
|
||||
"addFiles": "Add Files"
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "Upload PDF Files",
|
||||
"subtitle": "Add files to your storage for easy access across tools",
|
||||
@@ -3089,6 +3200,7 @@
|
||||
"lastModified": "Last Modified",
|
||||
"toolChain": "Tools Applied",
|
||||
"restore": "Restore",
|
||||
"unzip": "Unzip",
|
||||
"searchFiles": "Search files...",
|
||||
"recent": "Recent",
|
||||
"localFiles": "Local Files",
|
||||
@@ -3096,7 +3208,6 @@
|
||||
"googleDriveShort": "Drive",
|
||||
"myFiles": "My Files",
|
||||
"noRecentFiles": "No recent files found",
|
||||
"dropFilesHint": "Drop files here to upload",
|
||||
"googleDriveNotAvailable": "Google Drive integration not available",
|
||||
"openFiles": "Open Files",
|
||||
"openFile": "Open File",
|
||||
@@ -3369,6 +3480,16 @@
|
||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"firstPage": "First Page",
|
||||
"lastPage": "Last Page",
|
||||
"previousPage": "Previous Page",
|
||||
"nextPage": "Next Page",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"singlePageView": "Single Page View",
|
||||
"dualPageView": "Dual Page View"
|
||||
},
|
||||
"common": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
@@ -3439,4 +3560,4 @@
|
||||
},
|
||||
"termsAndConditions": "Terms & Conditions",
|
||||
"logOut": "Log out"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@
|
||||
"adjustContrast": {
|
||||
"tags": "contrast,brightness,saturation",
|
||||
"title": "Adjust Colors/Contrast",
|
||||
"desc": "Adjust Contrast, Saturation and Brightness of a PDF"
|
||||
"desc": "Adjust Colors/Contrast, Saturation and Brightness of a PDF"
|
||||
},
|
||||
"crop": {
|
||||
"tags": "trim,cut,resize",
|
||||
@@ -1243,7 +1243,17 @@
|
||||
"tags": "comments,highlight,notes,markup,remove",
|
||||
"title": "Remove Annotations",
|
||||
"header": "Remove Annotations",
|
||||
"submit": "Remove"
|
||||
"submit": "Remove",
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
},
|
||||
"info": {
|
||||
"title": "About Remove Annotations",
|
||||
"description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while removing annotations from the PDF."
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differentiate,contrast,changes,analysis",
|
||||
@@ -1702,8 +1712,9 @@
|
||||
"submit": "Sanitize PDF"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Adjust Contrast",
|
||||
"header": "Adjust Contrast",
|
||||
"title": "Adjust Colors/Contrast",
|
||||
"header": "Adjust Colors/Contrast",
|
||||
"basic": "Basic Adjustments",
|
||||
"contrast": "Contrast:",
|
||||
"brightness": "Brightness:",
|
||||
"saturation": "Saturation:",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -87,7 +87,10 @@
|
||||
"showStack": "Mostra traccia dello stack",
|
||||
"copyStack": "Copia traccia dello stack",
|
||||
"githubSubmit": "GitHub: apri un ticket",
|
||||
"discordSubmit": "Discord: invia post di supporto"
|
||||
"discordSubmit": "Discord: invia post di supporto",
|
||||
"dismissAllErrors": "Chiudi tutti gli errori",
|
||||
"encryptedPdfMustRemovePassword": "Questo PDF è crittografato o protetto da password. Si prega di sbloccarlo prima di convertire in PDF/A.",
|
||||
"incorrectPasswordProvided": "La password del PDF è errata o non è stata fornita."
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "Avviso"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "Ordinamento:",
|
||||
"multiTool": {
|
||||
"title": "Multifunzione PDF",
|
||||
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine"
|
||||
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine",
|
||||
"tags": "multipli,strumenti"
|
||||
},
|
||||
"merge": {
|
||||
"title": "Unisci",
|
||||
"desc": "Unisci facilmente più PDF in uno."
|
||||
"desc": "Unisci facilmente più PDF in uno.",
|
||||
"tags": "combina,unisci,unifica"
|
||||
},
|
||||
"split": {
|
||||
"title": "Dividi",
|
||||
"desc": "Dividi un singolo PDF in più documenti."
|
||||
"desc": "Dividi un singolo PDF in più documenti.",
|
||||
"tags": "dividi,separa,spezza"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "Ruota",
|
||||
"desc": "Ruota un PDF."
|
||||
"desc": "Ruota un PDF.",
|
||||
"tags": "ruota,capovolgi,orienta"
|
||||
},
|
||||
"convert": {
|
||||
"title": "Converti",
|
||||
"desc": "Converti file tra diversi formati"
|
||||
"desc": "Converti file tra diversi formati",
|
||||
"tags": "trasforma,cambia"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "Organizza",
|
||||
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine."
|
||||
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine.",
|
||||
"tags": "organizza,riordina,riorganizza"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "Aggiungi Immagine",
|
||||
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)"
|
||||
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)",
|
||||
"tags": "inserisci,incorpora,posiziona"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "Aggiungi allegati",
|
||||
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF"
|
||||
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF",
|
||||
"tags": "incorpora,allega,includi"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Aggiungi Filigrana",
|
||||
"desc": "Aggiungi una filigrana al tuo PDF."
|
||||
"desc": "Aggiungi una filigrana al tuo PDF.",
|
||||
"tags": "timbro,marca,sovrapponi"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "Rimuovi Password",
|
||||
"desc": "Rimuovi la password dal tuo PDF."
|
||||
"desc": "Rimuovi la password dal tuo PDF.",
|
||||
"tags": "sblocca"
|
||||
},
|
||||
"compress": {
|
||||
"title": "Comprimi",
|
||||
"desc": "Comprimi PDF per ridurne le dimensioni."
|
||||
"desc": "Comprimi PDF per ridurne le dimensioni.",
|
||||
"tags": "riduci,comprimi,ottimizza"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Sblocca moduli PDF",
|
||||
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF."
|
||||
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF.",
|
||||
"tags": "sblocca,abilita,modifica"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "Modifica Proprietà",
|
||||
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF."
|
||||
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.",
|
||||
"tags": "modifica,cambia,aggiorna"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "OCR / Pulisci scansioni",
|
||||
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro."
|
||||
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro.",
|
||||
"tags": "estrai,scansiona"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "Estrai immagini",
|
||||
"desc": "Estrai tutte le immagini da un PDF e salvale come zip."
|
||||
"desc": "Estrai tutte le immagini da un PDF e salvale come zip.",
|
||||
"tags": "estrai,salva,esporta"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "Rileva/Dividi foto scansionate",
|
||||
"desc": "Divide più foto all’interno di una foto/PDF"
|
||||
"desc": "Divide più foto all’interno di una foto/PDF",
|
||||
"tags": "rileva,dividi,foto"
|
||||
},
|
||||
"sign": {
|
||||
"title": "Firma",
|
||||
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine."
|
||||
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine.",
|
||||
"tags": "firma,autografo"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Appiattisci",
|
||||
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF."
|
||||
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF.",
|
||||
"tags": "semplifica,rimuovi,interattivo"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "Firma con certificato",
|
||||
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)"
|
||||
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)",
|
||||
"tags": "autentica,PEM,P12,ufficiale,cripta,firma,certificato,PKCS12,JKS,server,manuale,auto"
|
||||
},
|
||||
"repair": {
|
||||
"title": "Ripara",
|
||||
"desc": "Prova a riparare un PDF corrotto."
|
||||
"desc": "Prova a riparare un PDF corrotto.",
|
||||
"tags": "ripara,ripristina"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "Rimuovi pagine vuote",
|
||||
"desc": "Trova e rimuovi pagine vuote da un PDF."
|
||||
"desc": "Trova e rimuovi pagine vuote da un PDF.",
|
||||
"tags": "elimina,pulisci,vuote"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "Rimuovi annotazioni",
|
||||
"desc": "Rimuove tutti i commenti/annotazioni da un PDF"
|
||||
"desc": "Rimuove tutti i commenti/annotazioni da un PDF",
|
||||
"tags": "elimina,pulisci,rimuovi"
|
||||
},
|
||||
"compare": {
|
||||
"title": "Compara",
|
||||
"desc": "Vedi e compara le differenze tra due PDF."
|
||||
"desc": "Vedi e compara le differenze tra due PDF.",
|
||||
"tags": "differenza"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "Rimuovere firma dal certificato",
|
||||
"desc": "Rimuovi la firma del certificato dal PDF"
|
||||
"desc": "Rimuovi la firma del certificato dal PDF",
|
||||
"tags": "rimuovi,elimina,sblocca"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "Layout multipagina",
|
||||
"desc": "Unisci più pagine di un documento PDF in un'unica pagina"
|
||||
"desc": "Unisci più pagine di un documento PDF in un'unica pagina",
|
||||
"tags": "layout,disponi,combina"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Imposizione a libretto",
|
||||
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura"
|
||||
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura",
|
||||
"tags": "opuscolo,stampa,rilegatura"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Regola le dimensioni/scala della pagina",
|
||||
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti."
|
||||
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti.",
|
||||
"tags": "ridimensiona,adatta,scala"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "Aggiungi numeri di pagina",
|
||||
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita"
|
||||
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita",
|
||||
"tags": "numero,paginazione,conteggio"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "Rinomina automatica file PDF",
|
||||
"desc": "Rinomina automaticamente un file PDF in base all’intestazione rilevata"
|
||||
"desc": "Rinomina automaticamente un file PDF in base all’intestazione rilevata",
|
||||
"tags": "auto-rilevamento,basato su intestazione,organizza,rinomina"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Regola colori/contrasto",
|
||||
"desc": "Regola contrasto, saturazione e luminosità di un PDF"
|
||||
"desc": "Regola contrasto, saturazione e luminosità di un PDF",
|
||||
"tags": "contrasto,luminosità,saturazione"
|
||||
},
|
||||
"crop": {
|
||||
"title": "Ritaglia PDF",
|
||||
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)"
|
||||
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)",
|
||||
"tags": "ritaglia,taglia,ridimensiona"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "Pagine divise automaticamente",
|
||||
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato"
|
||||
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato",
|
||||
"tags": "auto,dividi,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Sanitizza",
|
||||
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF"
|
||||
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF",
|
||||
"tags": "pulisci,elimina,rimuovi"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "Ottieni TUTTE le informazioni in PDF",
|
||||
"desc": "Raccogli tutte le informazioni possibili sui PDF"
|
||||
"desc": "Raccogli tutte le informazioni possibili sui PDF",
|
||||
"tags": "info,metadati,dettagli"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF in un'unica pagina di grandi dimensioni",
|
||||
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina"
|
||||
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina",
|
||||
"tags": "combina,unisci,singola"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "Mostra Javascript",
|
||||
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF"
|
||||
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF",
|
||||
"tags": "javascript,codice,script"
|
||||
},
|
||||
"redact": {
|
||||
"title": "Redazione manuale",
|
||||
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)"
|
||||
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)",
|
||||
"tags": "censura,oscura,nascondi"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "Sovrapponi PDF",
|
||||
"desc": "Sovrapponi PDF sopra un altro PDF"
|
||||
"desc": "Sovrapponi PDF sopra un altro PDF",
|
||||
"tags": "sovrapponi,combina,impila"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "Dividi PDF per sezioni",
|
||||
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole"
|
||||
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole",
|
||||
"tags": "dividi,sezioni,separa"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "Aggiungi timbro al PDF",
|
||||
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche"
|
||||
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche",
|
||||
"tags": "timbro,marca,sigillo"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "Rimuovi immagine",
|
||||
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file"
|
||||
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file",
|
||||
"tags": "rimuovi,elimina,pulisci"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "Dividi PDF per capitoli",
|
||||
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli."
|
||||
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli.",
|
||||
"tags": "dividi,capitoli,struttura"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "Convalida la firma PDF",
|
||||
"desc": "Verificare le firme digitali e i certificati nei documenti PDF"
|
||||
"desc": "Verificare le firme digitali e i certificati nei documenti PDF",
|
||||
"tags": "convalida,verifica,certificato"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "Documentazione API",
|
||||
"desc": "Visualizza documentazione API e testa gli endpoint"
|
||||
"desc": "Visualizza documentazione API e testa gli endpoint",
|
||||
"tags": "API,documentazione,test"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Finta scansione",
|
||||
@@ -538,31 +585,38 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Modifica indice",
|
||||
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF"
|
||||
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF",
|
||||
"tags": "segnalibri,contenuti,modifica"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Gestisci certificati",
|
||||
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF."
|
||||
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF.",
|
||||
"tags": "certificati,importa,esporta"
|
||||
},
|
||||
"read": {
|
||||
"title": "Leggi",
|
||||
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione."
|
||||
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione.",
|
||||
"tags": "visualizza,apri,mostra"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Riorganizza pagine",
|
||||
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo drag‑and‑drop."
|
||||
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo drag‑and‑drop.",
|
||||
"tags": "riordina,riorganizza,organizza"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "Estrai pagine",
|
||||
"desc": "Estrai pagine specifiche da un PDF"
|
||||
"desc": "Estrai pagine specifiche da un PDF",
|
||||
"tags": "estrai,seleziona,copia"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "Rimuovi",
|
||||
"desc": "Elimina alcune pagine dal PDF."
|
||||
"desc": "Elimina alcune pagine dal PDF.",
|
||||
"tags": "elimina,estrai,escludi"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "Divisione automatica per dimensione/numero",
|
||||
"desc": "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti"
|
||||
"desc": "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti",
|
||||
"tags": "auto,dividi,dimensione"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Sostituisci e inverti il colore",
|
||||
@@ -570,11 +624,13 @@
|
||||
},
|
||||
"devApi": {
|
||||
"title": "API",
|
||||
"desc": "Link alla documentazione API"
|
||||
"desc": "Link alla documentazione API",
|
||||
"tags": "API,sviluppo,documentazione"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "Scansione cartelle automatizzata",
|
||||
"desc": "Link alla guida per scansione cartelle automatizzata"
|
||||
"desc": "Link alla guida per scansione cartelle automatizzata",
|
||||
"tags": "automazione,cartella,scansione"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "Guida SSO",
|
||||
@@ -594,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automatizza",
|
||||
"desc": "Crea flussi multi‑step concatenando azioni PDF. Ideale per attività ricorrenti."
|
||||
"desc": "Crea flussi multi‑step concatenando azioni PDF. Ideale per attività ricorrenti.",
|
||||
"tags": "flusso di lavoro,sequenza,automazione"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "Sostituisci o inverti i colori nei documenti PDF",
|
||||
"title": "Sostituisci e inverti colore"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "Crea un PDF che sembra essere stato scansionato",
|
||||
"tags": "scansiona,simula,crea",
|
||||
"title": "Effetto scanner"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -654,7 +720,9 @@
|
||||
},
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l’unione dei PDF."
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": "Generare l'indice nel file unito?",
|
||||
"removeDigitalSignature": "Rimuovere la firma digitale nel file unito?"
|
||||
},
|
||||
"split": {
|
||||
"tags": "Operazioni sulla pagina,divisione,multi pagina,taglio,lato server",
|
||||
@@ -913,7 +981,20 @@
|
||||
"10": "Unione pari-dispari",
|
||||
"11": "Duplica tutte le pagine"
|
||||
},
|
||||
"placeholder": "(ad es. 1,3,2 o 4-8,2,10-12 o 2n-1)"
|
||||
"placeholder": "(ad es. 1,3,2 o 4-8,2,10-12 o 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "Disporre le pagine per la stampa a opuscolo (ultima, prima, seconda, penultima, …).",
|
||||
"CUSTOM": "Utilizzare una sequenza personalizzata di numeri di pagina o espressioni per definire un nuovo ordine.",
|
||||
"DUPLEX_SORT": "Alternare fronte e retro come se uno scanner duplex avesse scansionato tutti i fronti, poi tutti i retri (1, n, 2, n-1, …).",
|
||||
"DUPLICATE": "Duplicare ogni pagina secondo il conteggio dell'ordine personalizzato (ad es., 4 duplica ogni pagina 4×).",
|
||||
"ODD_EVEN_MERGE": "Unire due PDF alternando le pagine: dispari dal primo, pari dal secondo.",
|
||||
"ODD_EVEN_SPLIT": "Dividere il documento in due output: tutte le pagine dispari e tutte le pagine pari.",
|
||||
"REMOVE_FIRST": "Rimuovere la prima pagina dal documento.",
|
||||
"REMOVE_FIRST_AND_LAST": "Rimuovere sia la prima che l'ultima pagina dal documento.",
|
||||
"REMOVE_LAST": "Rimuovere l'ultima pagina dal documento.",
|
||||
"REVERSE_ORDER": "Capovolgere il documento in modo che l'ultima pagina diventi la prima e così via.",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "Disporre le pagine per la stampa a opuscolo con cucitura laterale (ottimizzato per la rilegatura sul lato)."
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "img,jpg,immagine,foto",
|
||||
@@ -1347,7 +1428,7 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "Stato Trapped",
|
||||
"unknown": "Unknown",
|
||||
"unknown": "Sconosciuto",
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
@@ -1538,7 +1619,13 @@
|
||||
"header": "Estrai immagini",
|
||||
"selectText": "Seleziona il formato in cui salvare le immagini estratte",
|
||||
"allowDuplicates": "Salva le immagini duplicate",
|
||||
"submit": "Estrai"
|
||||
"submit": "Estrai",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l'estrazione delle immagini dal PDF."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "archivio,a lungo termine,standard,conversione,archiviazione,conservazione",
|
||||
@@ -1615,8 +1702,14 @@
|
||||
"title": "Firma",
|
||||
"header": "Firma PDF",
|
||||
"upload": "Carica immagine",
|
||||
"draw": "Disegna Firma",
|
||||
"text": "Testo",
|
||||
"draw": {
|
||||
"clear": "Cancella",
|
||||
"title": "Disegna la tua firma"
|
||||
},
|
||||
"text": {
|
||||
"name": "Nome firmatario",
|
||||
"placeholder": "Inserisci il tuo nome completo"
|
||||
},
|
||||
"clear": "Cancella",
|
||||
"add": "Aggiungi",
|
||||
"saved": "Firme salvate",
|
||||
@@ -1632,7 +1725,35 @@
|
||||
"previous": "Pagina precedente",
|
||||
"maintainRatio": "Attiva il mantenimento delle proporzioni",
|
||||
"undo": "Annulla",
|
||||
"redo": "Rifare"
|
||||
"redo": "Rifare",
|
||||
"activate": "Attiva posizionamento firma",
|
||||
"applySignatures": "Applica firme",
|
||||
"deactivate": "Interrompi posizionamento firme",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante la firma del PDF."
|
||||
},
|
||||
"image": {
|
||||
"hint": "Carica un'immagine PNG o JPG della tua firma",
|
||||
"label": "Carica immagine firma",
|
||||
"placeholder": "Seleziona file immagine"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "Come aggiungere la firma"
|
||||
},
|
||||
"results": {
|
||||
"title": "Risultati firma"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "Configura firma"
|
||||
},
|
||||
"submit": "Firma documento",
|
||||
"type": {
|
||||
"canvas": "Canvas",
|
||||
"draw": "Disegna",
|
||||
"image": "Immagine",
|
||||
"text": "Testo",
|
||||
"title": "Tipo di firma"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "statico,disattivato,non interattivo,ottimizzato",
|
||||
@@ -1651,7 +1772,8 @@
|
||||
"stepTitle": "Opzioni di flattening",
|
||||
"title": "Opzioni di flattening",
|
||||
"flattenOnlyForms.desc": "Appiattisci solo i campi modulo, lasciando intatti gli altri elementi interattivi",
|
||||
"note": "Il flattening rimuove gli elementi interattivi dal PDF, rendendoli non modificabili."
|
||||
"note": "Il flattening rimuove gli elementi interattivi dal PDF, rendendoli non modificabili.",
|
||||
"flattenOnlyForms": "Appiattisci solo i moduli"
|
||||
},
|
||||
"results": {
|
||||
"title": "Risultati Flatten"
|
||||
@@ -1747,7 +1869,17 @@
|
||||
"tags": "commenti,evidenziazioni,note,markup,rimozione",
|
||||
"title": "Rimuovi Annotazioni",
|
||||
"header": "Rimuovi Annotazioni",
|
||||
"submit": "Rimuovi"
|
||||
"submit": "Rimuovi",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante la rimozione delle annotazioni dal PDF."
|
||||
},
|
||||
"info": {
|
||||
"description": "Questo strumento rimuoverà tutte le annotazioni (commenti, evidenziazioni, note, ecc.) dai tuoi documenti PDF.",
|
||||
"title": "Informazioni su Rimuovi annotazioni"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differenziare,contrastare,cambiare,analisi",
|
||||
@@ -3024,7 +3156,13 @@
|
||||
"removeXMPMetadata.desc": "Rimuovi i metadati XMP dal PDF",
|
||||
"removeMetadata.desc": "Rimuovi le informazioni (titolo, autore, ecc.)",
|
||||
"removeLinks.desc": "Rimuovi link esterni e azioni di avvio dal PDF",
|
||||
"removeFonts.desc": "Rimuovi i font incorporati dal PDF"
|
||||
"removeFonts.desc": "Rimuovi i font incorporati dal PDF",
|
||||
"removeEmbeddedFiles": "Rimuovi file incorporati",
|
||||
"removeFonts": "Rimuovi caratteri",
|
||||
"removeJavaScript": "Rimuovi JavaScript",
|
||||
"removeLinks": "Rimuovi collegamenti",
|
||||
"removeMetadata": "Rimuovi metadati documento",
|
||||
"removeXMPMetadata": "Rimuovi metadati XMP"
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3294,5 +3432,56 @@
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "Termini e condizioni",
|
||||
"logOut": "Esci"
|
||||
"logOut": "Esci",
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "Aggiungi altri file...",
|
||||
"attachments": "Seleziona allegati",
|
||||
"info": "Seleziona i file da allegare al tuo PDF. Questi file saranno incorporati e accessibili tramite il pannello allegati del PDF.",
|
||||
"placeholder": "Scegli file...",
|
||||
"results": {
|
||||
"title": "Risultati allegati"
|
||||
},
|
||||
"selectFiles": "Seleziona file da allegare",
|
||||
"selectedFiles": "File selezionati",
|
||||
"submit": "Aggiungi allegati"
|
||||
},
|
||||
"applyAndContinue": "Applica e continua",
|
||||
"discardChanges": "Scarta modifiche",
|
||||
"exportAndContinue": "Esporta e continua",
|
||||
"keepWorking": "Continua a lavorare",
|
||||
"replaceColor": {
|
||||
"tags": "Sostituisci colore,Operazioni pagina,Back end,lato server"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l'estrazione delle scansioni di immagini."
|
||||
},
|
||||
"submit": "Estrai scansioni di immagini",
|
||||
"title": "Immagini estratte",
|
||||
"tooltip": {
|
||||
"headsUp": "Attenzione",
|
||||
"headsUpDesc": "Foto sovrapposte o sfondi molto simili nel colore alle foto possono ridurre la precisione - prova uno sfondo più chiaro o più scuro e lascia più spazio.",
|
||||
"problem1": "Foto non rilevate → aumentare la tolleranza a 30-50",
|
||||
"problem2": "Troppe rilevazioni errate → aumentare l'area minima a 15.000-20.000",
|
||||
"problem3": "I ritagli sono troppo stretti → aumentare la dimensione del bordo a 5-10",
|
||||
"problem4": "Foto inclinate non raddrizzate → abbassare la soglia angolare a ~5°",
|
||||
"problem5": "Caselle di polvere/rumore → aumentare l'area minima del contorno a 1000-2000",
|
||||
"quickFixes": "Correzioni rapide",
|
||||
"setupTips": "Suggerimenti di configurazione",
|
||||
"tip1": "Usa uno sfondo semplice e chiaro",
|
||||
"tip2": "Lascia un piccolo spazio (≈1 cm) tra le foto",
|
||||
"tip3": "Scansiona a 300-600 DPI",
|
||||
"tip4": "Pulisci il vetro dello scanner",
|
||||
"title": "Divisore di foto",
|
||||
"useCase1": "Scansiona intere pagine di album in una volta",
|
||||
"useCase2": "Dividi i lotti flatbed in file separati",
|
||||
"useCase3": "Suddividi collage in singole foto",
|
||||
"useCase4": "Estrai foto dai documenti",
|
||||
"whatThisDoes": "Cosa fa",
|
||||
"whatThisDoesDesc": "Trova ed estrae automaticamente ogni foto da una pagina scansionata o da un'immagine composita - senza ritaglio manuale.",
|
||||
"whenToUse": "Quando usare"
|
||||
}
|
||||
},
|
||||
"unsavedChanges": "Hai modifiche non salvate al tuo PDF. Cosa vuoi fare?",
|
||||
"unsavedChangesTitle": "Modifiche non salvate"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -87,7 +87,10 @@
|
||||
"showStack": "显示堆栈跟踪",
|
||||
"copyStack": "复制堆栈跟踪",
|
||||
"githubSubmit": "GitHub - 提交工单",
|
||||
"discordSubmit": "Discord - 提交支持帖子"
|
||||
"discordSubmit": "Discord - 提交支持帖子",
|
||||
"dismissAllErrors": "关闭所有错误",
|
||||
"encryptedPdfMustRemovePassword": "此 PDF 已加密或受密码保护。请在转换为 PDF/A 之前将其解锁。",
|
||||
"incorrectPasswordProvided": "PDF 密码不正确或未提供。"
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "警告"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "排序:",
|
||||
"multiTool": {
|
||||
"title": "PDF 多功能工具",
|
||||
"desc": "合并、旋转、重新排列和删除 PDF 页面"
|
||||
"desc": "合并、旋转、重新排列和删除 PDF 页面",
|
||||
"tags": "多个,工具"
|
||||
},
|
||||
"merge": {
|
||||
"title": "合并",
|
||||
"desc": "轻松将多个 PDF 合并成一个。"
|
||||
"desc": "轻松将多个 PDF 合并成一个。",
|
||||
"tags": "组合,合并,联合"
|
||||
},
|
||||
"split": {
|
||||
"title": "拆分",
|
||||
"desc": "将 PDF 拆分为多个文档。"
|
||||
"desc": "将 PDF 拆分为多个文档。",
|
||||
"tags": "分割,分离,拆分"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "旋转",
|
||||
"desc": "旋转 PDF。"
|
||||
"desc": "旋转 PDF。",
|
||||
"tags": "旋转,翻转,定向"
|
||||
},
|
||||
"convert": {
|
||||
"title": "转换",
|
||||
"desc": "在不同格式之间转换文件"
|
||||
"desc": "在不同格式之间转换文件",
|
||||
"tags": "转换,更改"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "整理",
|
||||
"desc": "按任意顺序删除/重新排列页面。"
|
||||
"desc": "按任意顺序删除/重新排列页面。",
|
||||
"tags": "组织,重新排列,重新排序"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "在 PDF 中添加图片",
|
||||
"desc": "将图像添加到 PDF 的指定位置。"
|
||||
"desc": "将图像添加到 PDF 的指定位置。",
|
||||
"tags": "插入,嵌入,放置"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "添加附件",
|
||||
"desc": "向 PDF 添加或移除嵌入文件(附件)"
|
||||
"desc": "向 PDF 添加或移除嵌入文件(附件)",
|
||||
"tags": "嵌入,附加,包含"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "添加水印",
|
||||
"desc": "在 PDF 中添加自定义水印。"
|
||||
"desc": "在 PDF 中添加自定义水印。",
|
||||
"tags": "印章,标记,叠加"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "删除密码",
|
||||
"desc": "从 PDF 文档中移除密码保护。"
|
||||
"desc": "从 PDF 文档中移除密码保护。",
|
||||
"tags": "解锁"
|
||||
},
|
||||
"compress": {
|
||||
"title": "压缩",
|
||||
"desc": "压缩 PDF 文件以减小文件大小。"
|
||||
"desc": "压缩 PDF 文件以减小文件大小。",
|
||||
"tags": "缩小,减少,优化"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "解锁PDF表单",
|
||||
"desc": "移除表单字段只读属性"
|
||||
"desc": "移除表单字段只读属性",
|
||||
"tags": "解锁,启用,编辑"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "更改元数据",
|
||||
"desc": "更改/删除/添加 PDF 文档的元数据。"
|
||||
"desc": "更改/删除/添加 PDF 文档的元数据。",
|
||||
"tags": "编辑,修改,更新"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "运行 OCR /清理扫描",
|
||||
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。"
|
||||
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。",
|
||||
"tags": "提取,扫描"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "提取图像",
|
||||
"desc": "从 PDF 中提取所有图像并保存到压缩包中。"
|
||||
"desc": "从 PDF 中提取所有图像并保存到压缩包中。",
|
||||
"tags": "提取,保存,导出"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "检测/拆分扫描照片",
|
||||
"desc": "从照片/PDF 中拆分出多张照片"
|
||||
"desc": "从照片/PDF 中拆分出多张照片",
|
||||
"tags": "检测,拆分,照片"
|
||||
},
|
||||
"sign": {
|
||||
"title": "签名",
|
||||
"desc": "通过绘图、文字或图像向 PDF 添加签名"
|
||||
"desc": "通过绘图、文字或图像向 PDF 添加签名",
|
||||
"tags": "签名,亲笔签名"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "展平",
|
||||
"desc": "从 PDF 中删除所有互动元素和表单"
|
||||
"desc": "从 PDF 中删除所有互动元素和表单",
|
||||
"tags": "简化,删除,交互式"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "使用证书签名",
|
||||
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名"
|
||||
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名",
|
||||
"tags": "认证,PEM,P12,官方,加密,签名,证书,PKCS12,JKS,服务器,手动,自动"
|
||||
},
|
||||
"repair": {
|
||||
"title": "修复",
|
||||
"desc": "尝试修复损坏/损坏的 PDF"
|
||||
"desc": "尝试修复损坏/损坏的 PDF",
|
||||
"tags": "修复,恢复"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "删除空白页",
|
||||
"desc": "检测并删除文档中的空白页"
|
||||
"desc": "检测并删除文档中的空白页",
|
||||
"tags": "删除,清理,空白"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "删除标注",
|
||||
"desc": "删除 PDF 中的所有标注/评论"
|
||||
"desc": "删除 PDF 中的所有标注/评论",
|
||||
"tags": "删除,清理,删除"
|
||||
},
|
||||
"compare": {
|
||||
"title": "比较",
|
||||
"desc": "比较并显示两个 PDF 文档之间的差异"
|
||||
"desc": "比较并显示两个 PDF 文档之间的差异",
|
||||
"tags": "差异"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "移除证书签名",
|
||||
"desc": "移除 PDF 的证书签名"
|
||||
"desc": "移除 PDF 的证书签名",
|
||||
"tags": "删除,删除,解锁"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "多页布局",
|
||||
"desc": "将 PDF 文档的多个页面合并成一页"
|
||||
"desc": "将 PDF 文档的多个页面合并成一页",
|
||||
"tags": "布局,排列,组合"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "小册子拼版",
|
||||
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订"
|
||||
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订",
|
||||
"tags": "小册子,打印,装订"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "调整页面尺寸/缩放",
|
||||
"desc": "调整页面及/或其内容的尺寸/缩放"
|
||||
"desc": "调整页面及/或其内容的尺寸/缩放",
|
||||
"tags": "调整大小,调整,缩放"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "添加页码",
|
||||
"desc": "在文档的指定位置添加页码"
|
||||
"desc": "在文档的指定位置添加页码",
|
||||
"tags": "编号,分页,计数"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "自动重命名 PDF 文件",
|
||||
"desc": "基于检测到的页眉自动重命名 PDF 文件"
|
||||
"desc": "基于检测到的页眉自动重命名 PDF 文件",
|
||||
"tags": "自动检测,基于标题,组织,重新标记"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "调整颜色/对比度",
|
||||
"desc": "调整 PDF 的对比度、饱和度和亮度"
|
||||
"desc": "调整 PDF 的对比度、饱和度和亮度",
|
||||
"tags": "对比度,亮度,饱和度"
|
||||
},
|
||||
"crop": {
|
||||
"title": "裁剪 PDF",
|
||||
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)"
|
||||
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)",
|
||||
"tags": "裁剪,剪切,调整大小"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "自动拆分页面",
|
||||
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF"
|
||||
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF",
|
||||
"tags": "自动,拆分,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "安全清理",
|
||||
"desc": "移除 PDF 文件中的潜在有害元素"
|
||||
"desc": "移除 PDF 文件中的潜在有害元素",
|
||||
"tags": "清理,清除,删除"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "获取 PDF 的所有信息",
|
||||
"desc": "获取 PDF 的所有可能的信息"
|
||||
"desc": "获取 PDF 的所有可能的信息",
|
||||
"tags": "信息,元数据,详细信息"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF 转单一大页",
|
||||
"desc": "将所有 PDF 页面合并为一个大的单页"
|
||||
"desc": "将所有 PDF 页面合并为一个大的单页",
|
||||
"tags": "组合,合并,单页"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "显示 JavaScript",
|
||||
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码"
|
||||
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码",
|
||||
"tags": "javascript,代码,脚本"
|
||||
},
|
||||
"redact": {
|
||||
"title": "手动修订",
|
||||
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF"
|
||||
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF",
|
||||
"tags": "审查,涂黑,隐藏"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "叠加 PDF",
|
||||
"desc": "将一个 PDF 叠加到另一个 PDF 之上"
|
||||
"desc": "将一个 PDF 叠加到另一个 PDF 之上",
|
||||
"tags": "叠加,组合,堆叠"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "按区块拆分 PDF",
|
||||
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块"
|
||||
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块",
|
||||
"tags": "拆分,部分,分割"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "向 PDF 添加印章",
|
||||
"desc": "在指定位置添加文本或图像印章"
|
||||
"desc": "在指定位置添加文本或图像印章",
|
||||
"tags": "印章,标记,盖章"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "删除图像",
|
||||
"desc": "删除图像减少 PDF 大小"
|
||||
"desc": "删除图像减少 PDF 大小",
|
||||
"tags": "删除,删除,清理"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "按章节拆分 PDF",
|
||||
"desc": "根据其章节结构将 PDF 拆分为多个文件。"
|
||||
"desc": "根据其章节结构将 PDF 拆分为多个文件。",
|
||||
"tags": "拆分,章节,结构"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "验证 PDF 签名",
|
||||
"desc": "验证 PDF 文档中的数字签名和证书"
|
||||
"desc": "验证 PDF 文档中的数字签名和证书",
|
||||
"tags": "验证,核实,证书"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API 文档",
|
||||
"desc": "查看 API 文档并测试端点"
|
||||
"desc": "查看 API 文档并测试端点",
|
||||
"tags": "API,文档,测试"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "伪扫描",
|
||||
@@ -538,31 +585,38 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "编辑目录",
|
||||
"desc": "为 PDF 文档添加或编辑目录和书签"
|
||||
"desc": "为 PDF 文档添加或编辑目录和书签",
|
||||
"tags": "书签,目录,编辑"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "管理证书",
|
||||
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。"
|
||||
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。",
|
||||
"tags": "证书,导入,导出"
|
||||
},
|
||||
"read": {
|
||||
"title": "阅读",
|
||||
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。"
|
||||
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。",
|
||||
"tags": "查看,打开,显示"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "重组页面",
|
||||
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。"
|
||||
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。",
|
||||
"tags": "重新排列,重新排序,组织"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "提取页面",
|
||||
"desc": "从 PDF 文档中提取特定页面"
|
||||
"desc": "从 PDF 文档中提取特定页面",
|
||||
"tags": "提取,选择,复制"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "删除",
|
||||
"desc": "从 PDF 文档中删除不需要的页面。"
|
||||
"desc": "从 PDF 文档中删除不需要的页面。",
|
||||
"tags": "删除,提取,排除"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "自动根据大小/数目拆分 PDF",
|
||||
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数"
|
||||
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数",
|
||||
"tags": "自动,拆分,大小"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "替换和反转颜色",
|
||||
@@ -570,11 +624,13 @@
|
||||
},
|
||||
"devApi": {
|
||||
"title": "API",
|
||||
"desc": "跳转至 API 文档"
|
||||
"desc": "跳转至 API 文档",
|
||||
"tags": "API,开发,文档"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "自动文件夹扫描",
|
||||
"desc": "跳转至自动文件夹扫描指南"
|
||||
"desc": "跳转至自动文件夹扫描指南",
|
||||
"tags": "自动化,文件夹,扫描"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "SSO 指南",
|
||||
@@ -594,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "自动化",
|
||||
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。"
|
||||
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。",
|
||||
"tags": "工作流,序列,自动化"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "替换或反转 PDF 文档中的颜色",
|
||||
"title": "替换和反转颜色"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "创建看起来像扫描的 PDF",
|
||||
"tags": "扫描,模拟,创建",
|
||||
"title": "扫描仪效果"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -654,7 +720,9 @@
|
||||
},
|
||||
"error": {
|
||||
"failed": "合并 PDF 时发生错误。"
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": "在合并的文件中生成目录?",
|
||||
"removeDigitalSignature": "在合并的文件中删除数字签名?"
|
||||
},
|
||||
"split": {
|
||||
"tags": "页面操作,划分,多页面,剪切,服务器端",
|
||||
@@ -815,7 +883,7 @@
|
||||
"settings": "设置",
|
||||
"conversionCompleted": "转换完成",
|
||||
"results": "结果",
|
||||
"defaultFilename": "converted_file",
|
||||
"defaultFilename": "已转换文件",
|
||||
"conversionResults": "转换结果",
|
||||
"convertFrom": "转换来源",
|
||||
"convertTo": "转换为",
|
||||
@@ -913,7 +981,20 @@
|
||||
"10": "奇偶合并",
|
||||
"11": "复制所有页面"
|
||||
},
|
||||
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1)"
|
||||
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "排列页面以进行小册子打印(最后,第一,第二,倒数第二,...)。",
|
||||
"CUSTOM": "使用自定义的页码序列或表达式来定义新顺序。",
|
||||
"DUPLEX_SORT": "交错正面然后背面,就像双面扫描仪扫描了所有正面,然后所有背面(1, n, 2, n-1, ...)。",
|
||||
"DUPLICATE": "根据自定义顺序计数复制每页(例如,4 复制每页 4×)。",
|
||||
"ODD_EVEN_MERGE": "通过交替页面合并两个 PDF:第一个的奇数页,第二个的偶数页。",
|
||||
"ODD_EVEN_SPLIT": "将文档拆分为两个输出:所有奇数页和所有偶数页。",
|
||||
"REMOVE_FIRST": "从文档中删除第一页。",
|
||||
"REMOVE_FIRST_AND_LAST": "从文档中删除第一页和最后一页。",
|
||||
"REMOVE_LAST": "从文档中删除最后一页。",
|
||||
"REVERSE_ORDER": "翻转文档,使最后一页变为第一页,依此类推。",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "排列页面以进行侧缝小册子打印(针对侧面装订进行了优化)。"
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "图像、JPG、图片、照片",
|
||||
@@ -937,7 +1018,7 @@
|
||||
"desc": "向 PDF 添加文本或图像水印",
|
||||
"completed": "已添加水印",
|
||||
"submit": "添加水印",
|
||||
"filenamePrefix": "watermarked",
|
||||
"filenamePrefix": "已加水印",
|
||||
"error": {
|
||||
"failed": "向 PDF 添加水印时发生错误。"
|
||||
},
|
||||
@@ -1136,7 +1217,7 @@
|
||||
"placeholder": "例如:1,3,5-8,10",
|
||||
"error": "无效的页码格式。使用数字、范围(1-5)或数学表达式(2n+1)"
|
||||
},
|
||||
"filenamePrefix": "pages_removed",
|
||||
"filenamePrefix": "已删除页面",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1284,7 +1365,7 @@
|
||||
"header": "解锁 PDF 表单",
|
||||
"submit": "Remove",
|
||||
"description": "该工具将移除 PDF 表单字段的只读限制,使其可编辑、可填写。",
|
||||
"filenamePrefix": "unlocked_forms",
|
||||
"filenamePrefix": "已解锁表单",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1299,7 +1380,7 @@
|
||||
"tags": "标题、作者、日期、创建、时间、发布者、制作人、统计数据",
|
||||
"header": "更改元数据",
|
||||
"submit": "更改",
|
||||
"filenamePrefix": "metadata",
|
||||
"filenamePrefix": "元数据",
|
||||
"settings": {
|
||||
"title": "元数据设置"
|
||||
},
|
||||
@@ -1347,7 +1428,7 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "陷印状态",
|
||||
"unknown": "Unknown",
|
||||
"unknown": "未知",
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
@@ -1538,7 +1619,13 @@
|
||||
"header": "提取图像",
|
||||
"selectText": "选择图像格式,将提取的图像转换为",
|
||||
"allowDuplicates": "保存重复图像",
|
||||
"submit": "提取"
|
||||
"submit": "提取",
|
||||
"error": {
|
||||
"failed": "从 PDF 提取图像时发生错误。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "归档、长期、标准、转换、存储、保存",
|
||||
@@ -1615,8 +1702,14 @@
|
||||
"title": "签名",
|
||||
"header": "签署 PDF",
|
||||
"upload": "上传图片",
|
||||
"draw": "绘制签名",
|
||||
"text": "文本输入",
|
||||
"draw": {
|
||||
"clear": "清除",
|
||||
"title": "绘制您的签名"
|
||||
},
|
||||
"text": {
|
||||
"name": "签署人姓名",
|
||||
"placeholder": "输入您的全名"
|
||||
},
|
||||
"clear": "清除",
|
||||
"add": "添加",
|
||||
"saved": "已保存签名",
|
||||
@@ -1632,7 +1725,35 @@
|
||||
"previous": "上一页",
|
||||
"maintainRatio": "切换保持长宽比",
|
||||
"undo": "撤销",
|
||||
"redo": "重做"
|
||||
"redo": "重做",
|
||||
"activate": "激活签名放置",
|
||||
"applySignatures": "应用签名",
|
||||
"deactivate": "停止放置签名",
|
||||
"error": {
|
||||
"failed": "签署 PDF 时发生错误。"
|
||||
},
|
||||
"image": {
|
||||
"hint": "上传 PNG 或 JPG 格式的签名图像",
|
||||
"label": "上传签名图像",
|
||||
"placeholder": "选择图像文件"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "如何添加签名"
|
||||
},
|
||||
"results": {
|
||||
"title": "签名结果"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "配置签名"
|
||||
},
|
||||
"submit": "签署文档",
|
||||
"type": {
|
||||
"canvas": "画布",
|
||||
"draw": "绘制",
|
||||
"image": "图像",
|
||||
"text": "文本",
|
||||
"title": "签名类型"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "静态、停用、非交互、简化",
|
||||
@@ -1640,7 +1761,7 @@
|
||||
"header": "展平 PDF",
|
||||
"flattenOnlyForms": "仅展平表格",
|
||||
"submit": "展平",
|
||||
"filenamePrefix": "flattened",
|
||||
"filenamePrefix": "已扁平化",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1651,7 +1772,8 @@
|
||||
"stepTitle": "扁平化选项",
|
||||
"title": "扁平化选项",
|
||||
"flattenOnlyForms.desc": "仅扁平化表单字段,保留其他交互元素",
|
||||
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。"
|
||||
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。",
|
||||
"flattenOnlyForms": "仅扁平化表单"
|
||||
},
|
||||
"results": {
|
||||
"title": "扁平化结果"
|
||||
@@ -1687,7 +1809,7 @@
|
||||
"header": "修复 PDF",
|
||||
"submit": "修复",
|
||||
"description": "该工具将尝试修复损坏或受损的 PDF 文件。无需额外设置。",
|
||||
"filenamePrefix": "repaired",
|
||||
"filenamePrefix": "已修复",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1747,7 +1869,17 @@
|
||||
"tags": "评论、高亮、笔记、标注、删除",
|
||||
"title": "删除标注",
|
||||
"header": "删除标注",
|
||||
"submit": "删除"
|
||||
"submit": "删除",
|
||||
"error": {
|
||||
"failed": "从 PDF 删除注释时发生错误。"
|
||||
},
|
||||
"info": {
|
||||
"description": "此工具将从您的 PDF 文档中删除所有注释(评论、高亮、笔记等)。",
|
||||
"title": "关于删除注释"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "区分、对比、更改、分析",
|
||||
@@ -1779,7 +1911,7 @@
|
||||
"certSign": {
|
||||
"tags": "身份验证、PEM、P12、官方、加密",
|
||||
"title": "证书签名",
|
||||
"filenamePrefix": "signed",
|
||||
"filenamePrefix": "已签名",
|
||||
"signMode": {
|
||||
"stepTitle": "签名模式",
|
||||
"tooltip": {
|
||||
@@ -1903,7 +2035,7 @@
|
||||
"selectPDF": "选择 PDF 文件:",
|
||||
"submit": "移除签名",
|
||||
"description": "该工具将从您的 PDF 文档中移除数字证书签名。",
|
||||
"filenamePrefix": "unsigned",
|
||||
"filenamePrefix": "未签名",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1923,7 +2055,7 @@
|
||||
"submit": "提交"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||
"tags": "小册子,拼版,打印,装订,折叠,签名",
|
||||
"title": "小册子拼版",
|
||||
"header": "小册子拼版",
|
||||
"submit": "创建小册子",
|
||||
@@ -2024,7 +2156,7 @@
|
||||
"submit": "提交"
|
||||
},
|
||||
"adjustPageScale": {
|
||||
"tags": "resize,modify,dimension,adapt",
|
||||
"tags": "调整大小,修改,尺寸,适应",
|
||||
"title": "调整页面比例",
|
||||
"header": "调整页面比例",
|
||||
"scaleFactor": {
|
||||
@@ -2547,7 +2679,7 @@
|
||||
"header": "将 PDF 转换为单页",
|
||||
"submit": "转为单页",
|
||||
"description": "该工具会将 PDF 的所有页面合并为一张超长单页。宽度保持与原页面相同,高度为所有页面高度之和。",
|
||||
"filenamePrefix": "single_page",
|
||||
"filenamePrefix": "单页",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -2768,7 +2900,7 @@
|
||||
"title": "API 文档",
|
||||
"header": "API 文档",
|
||||
"desc": "查看并测试 Stirling PDF 的 API 端点",
|
||||
"tags": "api,documentation,swagger,endpoints,development"
|
||||
"tags": "api,文档,swagger,端点,开发"
|
||||
},
|
||||
"cookieBanner": {
|
||||
"popUp": {
|
||||
@@ -3006,7 +3138,7 @@
|
||||
"completed": "安全清理成功完成",
|
||||
"error.generic": "安全清理失败",
|
||||
"error.failed": "安全清理 PDF 时发生错误。",
|
||||
"filenamePrefix": "sanitised",
|
||||
"filenamePrefix": "已清理",
|
||||
"sanitizationResults": "安全清理结果",
|
||||
"steps": {
|
||||
"files": "文件",
|
||||
@@ -3024,7 +3156,13 @@
|
||||
"removeXMPMetadata.desc": "从 PDF 中移除 XMP 元数据",
|
||||
"removeMetadata.desc": "移除文档信息元数据(标题、作者等)",
|
||||
"removeLinks.desc": "移除外部链接与启动动作",
|
||||
"removeFonts.desc": "从 PDF 中移除嵌入字体"
|
||||
"removeFonts.desc": "从 PDF 中移除嵌入字体",
|
||||
"removeEmbeddedFiles": "删除嵌入文件",
|
||||
"removeFonts": "删除字体",
|
||||
"removeJavaScript": "删除 JavaScript",
|
||||
"removeLinks": "删除链接",
|
||||
"removeMetadata": "删除文档元数据",
|
||||
"removeXMPMetadata": "删除 XMP 元数据"
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3032,7 +3170,7 @@
|
||||
"desc": "使用密码加密您的 PDF 文档。",
|
||||
"completed": "已应用密码保护",
|
||||
"submit": "加密",
|
||||
"filenamePrefix": "encrypted",
|
||||
"filenamePrefix": "已加密",
|
||||
"error": {
|
||||
"failed": "加密 PDF 时发生错误。"
|
||||
},
|
||||
@@ -3141,7 +3279,7 @@
|
||||
"placeholder": "输入当前密码",
|
||||
"completed": "密码已配置"
|
||||
},
|
||||
"filenamePrefix": "decrypted",
|
||||
"filenamePrefix": "已解密",
|
||||
"error": {
|
||||
"failed": "移除 PDF 密码时发生错误。"
|
||||
},
|
||||
@@ -3294,5 +3432,56 @@
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "条款与条件",
|
||||
"logOut": "退出登录"
|
||||
"logOut": "退出登录",
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "添加更多文件...",
|
||||
"attachments": "选择附件",
|
||||
"info": "选择要附加到 PDF 的文件。这些文件将被嵌入并可通过 PDF 的附件面板访问。",
|
||||
"placeholder": "选择文件...",
|
||||
"results": {
|
||||
"title": "附件结果"
|
||||
},
|
||||
"selectFiles": "选择要附加的文件",
|
||||
"selectedFiles": "已选择的文件",
|
||||
"submit": "添加附件"
|
||||
},
|
||||
"applyAndContinue": "应用并继续",
|
||||
"discardChanges": "放弃更改",
|
||||
"exportAndContinue": "导出并继续",
|
||||
"keepWorking": "继续工作",
|
||||
"replaceColor": {
|
||||
"tags": "替换颜色,页面操作,后端,服务器端"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "提取图像扫描时发生错误。"
|
||||
},
|
||||
"submit": "提取图像扫描",
|
||||
"title": "已提取的图像",
|
||||
"tooltip": {
|
||||
"headsUp": "注意",
|
||||
"headsUpDesc": "重叠的照片或颜色与照片非常接近的背景会降低准确性 - 尝试使用更浅或更深的背景并留出更多空间。",
|
||||
"problem1": "未检测到照片 → 将容差增加到 30-50",
|
||||
"problem2": "误检测太多 → 将最小面积增加到 15,000-20,000",
|
||||
"problem3": "裁剪太紧 → 将边框大小增加到 5-10",
|
||||
"problem4": "倾斜的照片未矫正 → 将角度阈值降低到 ~5°",
|
||||
"problem5": "灰尘/噪声框 → 将最小轮廓面积增加到 1000-2000",
|
||||
"quickFixes": "快速修复",
|
||||
"setupTips": "设置提示",
|
||||
"tip1": "使用简单的浅色背景",
|
||||
"tip2": "在照片之间留出小间隙(≈1 厘米)",
|
||||
"tip3": "以 300-600 DPI 扫描",
|
||||
"tip4": "清洁扫描仪玻璃",
|
||||
"title": "照片分割器",
|
||||
"useCase1": "一次扫描整个相册页面",
|
||||
"useCase2": "将平板批次拆分为单独的文件",
|
||||
"useCase3": "将拼贴画拆分为单独的照片",
|
||||
"useCase4": "从文档中提取照片",
|
||||
"whatThisDoes": "功能说明",
|
||||
"whatThisDoesDesc": "自动查找并从扫描页面或合成图像中提取每张照片 - 无需手动裁剪。",
|
||||
"whenToUse": "何时使用"
|
||||
}
|
||||
},
|
||||
"unsavedChanges": "您的 PDF 有未保存的更改。您想做什么?",
|
||||
"unsavedChangesTitle": "未保存的更改"
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const { execSync } = require('node:child_process');
|
||||
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
import { argv } from 'node:process';
|
||||
const { argv } = require('node:process');
|
||||
const inputIdx = argv.indexOf('--input');
|
||||
const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null;
|
||||
const POSTPROCESS_ONLY = !!INPUT_FILE;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
// __dirname is available in CommonJS by default
|
||||
|
||||
/**
|
||||
* Generate 3rd party licenses for frontend dependencies
|
||||
|
||||
+25
-16
@@ -1,10 +1,12 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
|
||||
import { FileContextProvider } from "./contexts/FileContext";
|
||||
import { NavigationProvider } from "./contexts/NavigationContext";
|
||||
import { FilesModalProvider } from "./contexts/FilesModalContext";
|
||||
import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
||||
import { HotkeyProvider } from "./contexts/HotkeyContext";
|
||||
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||
import { PreferencesProvider } from "./contexts/PreferencesContext";
|
||||
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
||||
import HomePage from "./pages/HomePage";
|
||||
|
||||
@@ -14,6 +16,7 @@ import "./styles/cookieconsent.css";
|
||||
import "./index.css";
|
||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
||||
import { SignatureProvider } from "./contexts/SignatureContext";
|
||||
|
||||
// Import file ID debugging helpers (development only)
|
||||
import "./utils/fileIdSafety";
|
||||
@@ -39,21 +42,27 @@ export default function App() {
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<RainbowThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</FileContextProvider>
|
||||
<PreferencesProvider>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</FileContextProvider>
|
||||
</PreferencesProvider>
|
||||
</ErrorBoundary>
|
||||
</RainbowThemeProvider>
|
||||
</Suspense>
|
||||
|
||||
@@ -7,6 +7,132 @@
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/core",
|
||||
"moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/engines",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-annotation",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-export",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-history",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-interaction-manager",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-loader",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-pan",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-render",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-rotate",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-scroll",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-search",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-selection",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-spread",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-thumbnail",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-tiling",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-viewport",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-zoom",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@emotion/react",
|
||||
"moduleUrl": "git+https://github.com/emotion-js/emotion.git#main",
|
||||
@@ -35,6 +161,13 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dates",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleVersion": "8.3.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dropzone",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
@@ -143,7 +276,7 @@
|
||||
{
|
||||
"moduleName": "posthog-js",
|
||||
"moduleUrl": "git+https://github.com/PostHog/posthog-js.git",
|
||||
"moduleVersion": "1.266.0",
|
||||
"moduleVersion": "1.268.0",
|
||||
"moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
|
||||
"moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,8 @@ import MobileLayout from './fileManager/MobileLayout';
|
||||
import DesktopLayout from './fileManager/DesktopLayout';
|
||||
import DragOverlay from './fileManager/DragOverlay';
|
||||
import { FileManagerProvider } from '../contexts/FileManagerContext';
|
||||
import { isGoogleDriveConfigured } from '../services/googleDrivePickerService';
|
||||
import { loadScript } from '../utils/scriptLoader';
|
||||
|
||||
interface FileManagerProps {
|
||||
selectedTool?: Tool | null;
|
||||
@@ -20,7 +22,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
const { loadRecentFiles, handleRemoveFile } = useFileManager();
|
||||
const { loadRecentFiles, handleRemoveFile, loading } = useFileManager();
|
||||
|
||||
// File management handlers
|
||||
const isFileSupported = useCallback((fileName: string) => {
|
||||
@@ -84,6 +86,29 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Preload Google Drive scripts if configured
|
||||
useEffect(() => {
|
||||
if (isGoogleDriveConfigured()) {
|
||||
// Load scripts in parallel without blocking
|
||||
Promise.all([
|
||||
loadScript({
|
||||
src: 'https://apis.google.com/js/api.js',
|
||||
id: 'gapi-script',
|
||||
async: true,
|
||||
defer: true,
|
||||
}),
|
||||
loadScript({
|
||||
src: 'https://accounts.google.com/gsi/client',
|
||||
id: 'gis-script',
|
||||
async: true,
|
||||
defer: true,
|
||||
}),
|
||||
]).catch((error) => {
|
||||
console.warn('Failed to preload Google Drive scripts:', error);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Modal size constants for consistent scaling
|
||||
const modalHeight = '80vh';
|
||||
const modalWidth = isMobile ? '100%' : '80vw';
|
||||
@@ -123,7 +148,6 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
onDrop={handleNewFileUpload}
|
||||
onDragEnter={() => setIsDragging(true)}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
accept={{}}
|
||||
multiple={true}
|
||||
activateOnClick={false}
|
||||
style={{
|
||||
@@ -147,6 +171,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
onFileRemove={handleRemoveFileByIndex}
|
||||
modalHeight={modalHeight}
|
||||
refreshRecentFiles={refreshRecentFiles}
|
||||
isLoading={loading}
|
||||
>
|
||||
{isMobile ? <MobileLayout /> : <DesktopLayout />}
|
||||
</FileManagerProvider>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
|
||||
interface PDFAnnotationContextValue {
|
||||
// Drawing mode management
|
||||
activateDrawMode: () => void;
|
||||
deactivateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
|
||||
// Drawing settings
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
|
||||
// History operations
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
|
||||
// Image data management
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
|
||||
// Placement state
|
||||
isPlacementMode: boolean;
|
||||
|
||||
// Signature configuration
|
||||
signatureConfig: any | null;
|
||||
setSignatureConfig: (config: any | null) => void;
|
||||
}
|
||||
|
||||
const PDFAnnotationContext = createContext<PDFAnnotationContextValue | undefined>(undefined);
|
||||
|
||||
interface PDFAnnotationProviderProps {
|
||||
children: ReactNode;
|
||||
// These would come from the signature context
|
||||
activateDrawMode: () => void;
|
||||
deactivateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
isPlacementMode: boolean;
|
||||
signatureConfig: any | null;
|
||||
setSignatureConfig: (config: any | null) => void;
|
||||
}
|
||||
|
||||
export const PDFAnnotationProvider: React.FC<PDFAnnotationProviderProps> = ({
|
||||
children,
|
||||
activateDrawMode,
|
||||
deactivateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
isPlacementMode,
|
||||
signatureConfig,
|
||||
setSignatureConfig
|
||||
}) => {
|
||||
const contextValue: PDFAnnotationContextValue = {
|
||||
activateDrawMode,
|
||||
deactivateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
isPlacementMode,
|
||||
signatureConfig,
|
||||
setSignatureConfig
|
||||
};
|
||||
|
||||
return (
|
||||
<PDFAnnotationContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</PDFAnnotationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePDFAnnotation = (): PDFAnnotationContextValue => {
|
||||
const context = useContext(PDFAnnotationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack, Alert, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DrawingControls } from './DrawingControls';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { usePDFAnnotation } from '../providers/PDFAnnotationProvider';
|
||||
|
||||
export interface AnnotationToolConfig {
|
||||
enableDrawing?: boolean;
|
||||
enableImageUpload?: boolean;
|
||||
enableTextInput?: boolean;
|
||||
showPlaceButton?: boolean;
|
||||
placeButtonText?: string;
|
||||
}
|
||||
|
||||
interface BaseAnnotationToolProps {
|
||||
config: AnnotationToolConfig;
|
||||
children: React.ReactNode;
|
||||
onSignatureDataChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const BaseAnnotationTool: React.FC<BaseAnnotationToolProps> = ({
|
||||
config,
|
||||
children,
|
||||
onSignatureDataChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
activateSignaturePlacementMode,
|
||||
undo,
|
||||
redo
|
||||
} = usePDFAnnotation();
|
||||
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||
|
||||
const handleSignatureDataChange = (data: string | null) => {
|
||||
setSignatureData(data);
|
||||
onSignatureDataChange?.(data);
|
||||
};
|
||||
|
||||
const handlePlaceSignature = () => {
|
||||
if (activateSignaturePlacementMode) {
|
||||
activateSignaturePlacementMode();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Drawing Controls (Undo/Redo/Place) */}
|
||||
<DrawingControls
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onPlaceSignature={config.showPlaceButton ? handlePlaceSignature : undefined}
|
||||
hasSignatureData={!!signatureData}
|
||||
disabled={disabled}
|
||||
showPlaceButton={config.showPlaceButton}
|
||||
placeButtonText={config.placeButtonText}
|
||||
/>
|
||||
|
||||
{/* Tool Content */}
|
||||
{React.cloneElement(children as React.ReactElement<any>, {
|
||||
selectedColor,
|
||||
signatureData,
|
||||
onSignatureDataChange: handleSignatureDataChange,
|
||||
onColorSwatchClick: () => setIsColorPickerOpen(true),
|
||||
disabled
|
||||
})}
|
||||
|
||||
{/* Instructions for placing signature */}
|
||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||
<Text size="sm">
|
||||
Click anywhere on the PDF to place your annotation.
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={setSelectedColor}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core';
|
||||
|
||||
interface ColorPickerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedColor: string;
|
||||
onColorChange: (color: string) => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectedColor,
|
||||
onColorChange,
|
||||
title = "Choose Color"
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
opened={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
size="sm"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<MantineColorPicker
|
||||
format="hex"
|
||||
value={selectedColor}
|
||||
onChange={onColorChange}
|
||||
swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']}
|
||||
swatchesPerRow={6}
|
||||
size="lg"
|
||||
fullWidth
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface ColorSwatchButtonProps {
|
||||
color: string;
|
||||
onClick: () => void;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const ColorSwatchButton: React.FC<ColorSwatchButtonProps> = ({
|
||||
color,
|
||||
onClick,
|
||||
size = 24
|
||||
}) => {
|
||||
return (
|
||||
<ColorSwatch
|
||||
color={color}
|
||||
size={size}
|
||||
radius={0}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,437 @@
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { Paper, Group, Button, Modal, Stack, Text } from '@mantine/core';
|
||||
import { ColorSwatchButton } from './ColorPicker';
|
||||
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
||||
|
||||
interface DrawingCanvasProps {
|
||||
selectedColor: string;
|
||||
penSize: number;
|
||||
penSizeInput: string;
|
||||
onColorSwatchClick: () => void;
|
||||
onPenSizeChange: (size: number) => void;
|
||||
onPenSizeInputChange: (input: string) => void;
|
||||
onSignatureDataChange: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
modalWidth?: number;
|
||||
modalHeight?: number;
|
||||
additionalButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
||||
selectedColor,
|
||||
penSize,
|
||||
penSizeInput,
|
||||
onColorSwatchClick,
|
||||
onPenSizeChange,
|
||||
onPenSizeInputChange,
|
||||
onSignatureDataChange,
|
||||
disabled = false,
|
||||
width = 400,
|
||||
height = 150,
|
||||
modalWidth = 800,
|
||||
modalHeight = 400,
|
||||
additionalButtons
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const visibleModalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [isModalDrawing, setIsModalDrawing] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// Drawing functions for main canvas
|
||||
const startDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!canvasRef.current || disabled) return;
|
||||
|
||||
setIsDrawing(true);
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const scaleX = canvasRef.current.width / rect.width;
|
||||
const scaleY = canvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
}
|
||||
}, [disabled, selectedColor, penSize]);
|
||||
|
||||
const draw = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!isDrawing || !canvasRef.current || disabled) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const scaleX = canvasRef.current.width / rect.width;
|
||||
const scaleY = canvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [isDrawing, disabled]);
|
||||
|
||||
const stopDrawing = useCallback(() => {
|
||||
if (!isDrawing || disabled) return;
|
||||
|
||||
setIsDrawing(false);
|
||||
|
||||
// Save canvas as signature data
|
||||
if (canvasRef.current) {
|
||||
const dataURL = canvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
}
|
||||
}, [isDrawing, disabled, onSignatureDataChange]);
|
||||
|
||||
// Modal canvas drawing functions
|
||||
const startModalDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||
|
||||
setIsModalDrawing(true);
|
||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Draw on both the visible modal canvas and hidden canvas
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
|
||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
}
|
||||
});
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
const drawModal = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!isModalDrawing || !visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||
|
||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Draw on both canvases
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
|
||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||
if (ctx) {
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}, [isModalDrawing]);
|
||||
|
||||
const stopModalDrawing = useCallback(() => {
|
||||
if (!isModalDrawing) return;
|
||||
setIsModalDrawing(false);
|
||||
|
||||
// Sync the canvases and update signature data (only when drawing stops)
|
||||
if (modalCanvasRef.current) {
|
||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
|
||||
// Also update the small canvas display
|
||||
if (canvasRef.current) {
|
||||
const smallCtx = canvasRef.current.getContext('2d');
|
||||
if (smallCtx) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
smallCtx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
smallCtx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
};
|
||||
img.src = dataURL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isModalDrawing]);
|
||||
|
||||
// Clear canvas functions
|
||||
const clearCanvas = useCallback(() => {
|
||||
if (!canvasRef.current || disabled) return;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
|
||||
// Also clear the modal canvas if it exists
|
||||
if (modalCanvasRef.current) {
|
||||
const modalCtx = modalCanvasRef.current.getContext('2d');
|
||||
if (modalCtx) {
|
||||
modalCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
onSignatureDataChange(null);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const clearModalCanvas = useCallback(() => {
|
||||
// Clear both modal canvases (visible and hidden)
|
||||
if (modalCanvasRef.current) {
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
if (hiddenCtx) {
|
||||
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleModalCanvasRef.current) {
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
if (visibleCtx) {
|
||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear the main canvas and signature data
|
||||
if (canvasRef.current) {
|
||||
const mainCtx = canvasRef.current.getContext('2d');
|
||||
if (mainCtx) {
|
||||
mainCtx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
onSignatureDataChange(null);
|
||||
}, []);
|
||||
|
||||
const saveModalSignature = useCallback(() => {
|
||||
if (!modalCanvasRef.current) return;
|
||||
|
||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
|
||||
// Copy to small canvas for display
|
||||
if (canvasRef.current) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
};
|
||||
img.src = dataURL;
|
||||
}
|
||||
}
|
||||
|
||||
setIsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
setIsModalOpen(true);
|
||||
// Copy content to modal canvas after a brief delay
|
||||
setTimeout(() => {
|
||||
if (visibleModalCanvasRef.current && modalCanvasRef.current) {
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
if (visibleCtx) {
|
||||
visibleCtx.strokeStyle = selectedColor;
|
||||
visibleCtx.lineWidth = penSize;
|
||||
visibleCtx.lineCap = 'round';
|
||||
visibleCtx.lineJoin = 'round';
|
||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
visibleCtx.drawImage(modalCanvasRef.current, 0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
// Initialize canvas settings whenever color or pen size changes
|
||||
React.useEffect(() => {
|
||||
const updateCanvas = (canvas: HTMLCanvasElement | null) => {
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
}
|
||||
};
|
||||
|
||||
updateCanvas(canvasRef.current);
|
||||
updateCanvas(modalCanvasRef.current);
|
||||
updateCanvas(visibleModalCanvasRef.current);
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper withBorder p="md">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Draw your signature</Text>
|
||||
<Group gap="lg">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs" ta="center">Color</Text>
|
||||
<Group justify="center">
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
onClick={onColorSwatchClick}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||
<PenSizeSelector
|
||||
value={penSize}
|
||||
inputValue={penSizeInput}
|
||||
onValueChange={onPenSizeChange}
|
||||
onInputChange={onPenSizeInputChange}
|
||||
disabled={disabled}
|
||||
placeholder="Size"
|
||||
size="compact-sm"
|
||||
style={{ width: '60px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingTop: '24px' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
size="compact-sm"
|
||||
onClick={openModal}
|
||||
disabled={disabled}
|
||||
>
|
||||
Expand
|
||||
</Button>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: disabled ? 'default' : 'crosshair',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
}}
|
||||
onMouseDown={startDrawing}
|
||||
onMouseMove={draw}
|
||||
onMouseUp={stopDrawing}
|
||||
onMouseLeave={stopDrawing}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
{additionalButtons}
|
||||
</div>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="compact-sm"
|
||||
onClick={clearCanvas}
|
||||
disabled={disabled}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Hidden canvas for modal synchronization */}
|
||||
<canvas
|
||||
ref={modalCanvasRef}
|
||||
width={modalWidth}
|
||||
height={modalHeight}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Modal for larger signature canvas */}
|
||||
<Modal
|
||||
opened={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title="Draw Your Signature"
|
||||
size="xl"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Color and Pen Size picker */}
|
||||
<Paper withBorder p="sm">
|
||||
<Group gap="lg" align="flex-end">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Color</Text>
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
onClick={onColorSwatchClick}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||
<PenSizeSelector
|
||||
value={penSize}
|
||||
inputValue={penSizeInput}
|
||||
onValueChange={onPenSizeChange}
|
||||
onInputChange={onPenSizeInputChange}
|
||||
placeholder="Size"
|
||||
size="compact-sm"
|
||||
style={{ width: '60px' }}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md">
|
||||
<canvas
|
||||
ref={visibleModalCanvasRef}
|
||||
width={modalWidth}
|
||||
height={modalHeight}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: 'crosshair',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
maxWidth: `${modalWidth}px`,
|
||||
height: 'auto',
|
||||
}}
|
||||
onMouseDown={startModalDrawing}
|
||||
onMouseMove={drawModal}
|
||||
onMouseUp={stopModalDrawing}
|
||||
onMouseLeave={stopModalDrawing}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={clearModalCanvas}
|
||||
>
|
||||
Clear Canvas
|
||||
</Button>
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveModalSignature}
|
||||
>
|
||||
Save Signature
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawingCanvas;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Group, Button } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DrawingControlsProps {
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
onPlaceSignature?: () => void;
|
||||
hasSignatureData?: boolean;
|
||||
disabled?: boolean;
|
||||
showPlaceButton?: boolean;
|
||||
placeButtonText?: string;
|
||||
}
|
||||
|
||||
export const DrawingControls: React.FC<DrawingControlsProps> = ({
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPlaceSignature,
|
||||
hasSignatureData = false,
|
||||
disabled = false,
|
||||
showPlaceButton = true,
|
||||
placeButtonText = "Update and Place"
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group gap="sm">
|
||||
{/* Undo/Redo Controls */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onUndo}
|
||||
disabled={disabled}
|
||||
flex={1}
|
||||
>
|
||||
{t('sign.undo', 'Undo')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRedo}
|
||||
disabled={disabled}
|
||||
flex={1}
|
||||
>
|
||||
{t('sign.redo', 'Redo')}
|
||||
</Button>
|
||||
|
||||
{/* Place Signature Button */}
|
||||
{showPlaceButton && onPlaceSignature && (
|
||||
<Button
|
||||
variant="filled"
|
||||
color="blue"
|
||||
onClick={onPlaceSignature}
|
||||
disabled={disabled || !hasSignatureData}
|
||||
flex={1}
|
||||
>
|
||||
{placeButtonText}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { FileInput, Text, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ImageUploaderProps {
|
||||
onImageChange: (file: File | null) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
onImageChange,
|
||||
disabled = false,
|
||||
label,
|
||||
placeholder,
|
||||
hint
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleImageChange = async (file: File | null) => {
|
||||
if (file && !disabled) {
|
||||
try {
|
||||
// Validate that it's actually an image file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
console.error('Selected file is not an image');
|
||||
return;
|
||||
}
|
||||
|
||||
onImageChange(file);
|
||||
} catch (error) {
|
||||
console.error('Error processing image file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
// Clear image data when no file is selected
|
||||
onImageChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<FileInput
|
||||
label={label || t('sign.image.label', 'Upload signature image')}
|
||||
placeholder={placeholder || t('sign.image.placeholder', 'Select image file')}
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm" c="dimmed">
|
||||
{hint || t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, TextInput, Select, Combobox, useCombobox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TextInputWithFontProps {
|
||||
text: string;
|
||||
onTextChange: (text: string) => void;
|
||||
fontSize: number;
|
||||
onFontSizeChange: (size: number) => void;
|
||||
fontFamily: string;
|
||||
onFontFamilyChange: (family: string) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
||||
text,
|
||||
onTextChange,
|
||||
fontSize,
|
||||
onFontSizeChange,
|
||||
fontFamily,
|
||||
onFontFamilyChange,
|
||||
disabled = false,
|
||||
label,
|
||||
placeholder
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
||||
const fontSizeCombobox = useCombobox();
|
||||
|
||||
// Sync font size input with prop changes
|
||||
useEffect(() => {
|
||||
setFontSizeInput(fontSize.toString());
|
||||
}, [fontSize]);
|
||||
|
||||
const fontOptions = [
|
||||
{ value: 'Helvetica', label: 'Helvetica' },
|
||||
{ value: 'Times-Roman', label: 'Times' },
|
||||
{ value: 'Courier', label: 'Courier' },
|
||||
{ value: 'Arial', label: 'Arial' },
|
||||
{ value: 'Georgia', label: 'Georgia' },
|
||||
];
|
||||
|
||||
const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48'];
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label={label || t('sign.text.name', 'Signer Name')}
|
||||
placeholder={placeholder || t('sign.text.placeholder', 'Enter your full name')}
|
||||
value={text}
|
||||
onChange={(e) => onTextChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Font Selection */}
|
||||
<Select
|
||||
label="Font"
|
||||
value={fontFamily}
|
||||
onChange={(value) => onFontFamilyChange(value || 'Helvetica')}
|
||||
data={fontOptions}
|
||||
disabled={disabled}
|
||||
searchable
|
||||
allowDeselect={false}
|
||||
/>
|
||||
|
||||
{/* Font Size */}
|
||||
<Combobox
|
||||
onOptionSubmit={(optionValue) => {
|
||||
setFontSizeInput(optionValue);
|
||||
const size = parseInt(optionValue);
|
||||
if (!isNaN(size)) {
|
||||
onFontSizeChange(size);
|
||||
}
|
||||
fontSizeCombobox.closeDropdown();
|
||||
}}
|
||||
store={fontSizeCombobox}
|
||||
withinPortal={false}
|
||||
>
|
||||
<Combobox.Target>
|
||||
<TextInput
|
||||
label="Font Size"
|
||||
placeholder="Type or select font size (8-72)"
|
||||
value={fontSizeInput}
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
setFontSizeInput(value);
|
||||
|
||||
// Parse and validate the typed value in real-time
|
||||
const size = parseInt(value);
|
||||
if (!isNaN(size) && size >= 8 && size <= 72) {
|
||||
onFontSizeChange(size);
|
||||
}
|
||||
|
||||
fontSizeCombobox.openDropdown();
|
||||
fontSizeCombobox.updateSelectedOptionIndex();
|
||||
}}
|
||||
onClick={() => fontSizeCombobox.openDropdown()}
|
||||
onFocus={() => fontSizeCombobox.openDropdown()}
|
||||
onBlur={() => {
|
||||
fontSizeCombobox.closeDropdown();
|
||||
// Clean up invalid values on blur
|
||||
const size = parseInt(fontSizeInput);
|
||||
if (isNaN(size) || size < 8 || size > 72) {
|
||||
setFontSizeInput(fontSize.toString());
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Combobox.Target>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>
|
||||
{fontSizeOptions.map((size) => (
|
||||
<Combobox.Option value={size} key={size}>
|
||||
{size}px
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { DrawingCanvas } from '../shared/DrawingCanvas';
|
||||
|
||||
interface DrawingToolProps {
|
||||
onDrawingChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DrawingTool: React.FC<DrawingToolProps> = ({
|
||||
onDrawingChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [selectedColor] = useState('#000000');
|
||||
const [penSize, setPenSize] = useState(2);
|
||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||
|
||||
const toolConfig = {
|
||||
enableDrawing: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Drawing"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={onDrawingChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<DrawingCanvas
|
||||
selectedColor={selectedColor}
|
||||
penSize={penSize}
|
||||
penSizeInput={penSizeInput}
|
||||
onColorSwatchClick={() => {}} // Color picker handled by BaseAnnotationTool
|
||||
onPenSizeChange={setPenSize}
|
||||
onPenSizeInputChange={setPenSizeInput}
|
||||
onSignatureDataChange={onDrawingChange || (() => {})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { ImageUploader } from '../shared/ImageUploader';
|
||||
|
||||
interface ImageToolProps {
|
||||
onImageChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ImageTool: React.FC<ImageToolProps> = ({
|
||||
onImageChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [, setImageData] = useState<string | null>(null);
|
||||
|
||||
const handleImageUpload = async (file: File | null) => {
|
||||
if (file && !disabled) {
|
||||
try {
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
resolve(e.target.result as string);
|
||||
} else {
|
||||
reject(new Error('Failed to read file'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
setImageData(result);
|
||||
onImageChange?.(result);
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
setImageData(null);
|
||||
onImageChange?.(null);
|
||||
}
|
||||
};
|
||||
|
||||
const toolConfig = {
|
||||
enableImageUpload: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Image"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={onImageChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<ImageUploader
|
||||
onImageChange={handleImageUpload}
|
||||
disabled={disabled}
|
||||
label="Upload Image"
|
||||
placeholder="Select image file"
|
||||
hint="Upload a PNG, JPG, or other image file to place on the PDF"
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { TextInputWithFont } from '../shared/TextInputWithFont';
|
||||
|
||||
interface TextToolProps {
|
||||
onTextChange?: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TextTool: React.FC<TextToolProps> = ({
|
||||
onTextChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [text, setText] = useState('');
|
||||
const [fontSize, setFontSize] = useState(16);
|
||||
const [fontFamily, setFontFamily] = useState('Helvetica');
|
||||
|
||||
const handleTextChange = (newText: string) => {
|
||||
setText(newText);
|
||||
onTextChange?.(newText);
|
||||
};
|
||||
|
||||
const handleSignatureDataChange = (data: string | null) => {
|
||||
if (data) {
|
||||
onTextChange?.(data);
|
||||
}
|
||||
};
|
||||
|
||||
const toolConfig = {
|
||||
enableTextInput: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Text"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={handleSignatureDataChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<TextInputWithFont
|
||||
text={text}
|
||||
onTextChange={handleTextChange}
|
||||
fontSize={fontSize}
|
||||
onFontSizeChange={setFontSize}
|
||||
fontFamily={fontFamily}
|
||||
onFontFamilyChange={setFontFamily}
|
||||
disabled={disabled}
|
||||
label="Text Content"
|
||||
placeholder="Enter text to place on the PDF"
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||
import LocalIcon from '../shared/LocalIcon';
|
||||
import { BASE_PATH } from '../../constants/app';
|
||||
import styles from './FileEditor.module.css';
|
||||
|
||||
interface AddFileCardProps {
|
||||
onFileSelect: (files: File[]) => void;
|
||||
accept?: string;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const AddFileCard = ({
|
||||
onFileSelect,
|
||||
accept,
|
||||
multiple = true
|
||||
}: AddFileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [isUploadHover, setIsUploadHover] = useState(false);
|
||||
|
||||
const handleCardClick = () => {
|
||||
openFilesModal();
|
||||
};
|
||||
|
||||
const handleNativeUploadClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const handleOpenFilesModal = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
openFilesModal();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (files.length > 0) {
|
||||
onFileSelect(files);
|
||||
}
|
||||
// Reset input so same files can be selected again
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`${styles.addFileCard} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative cursor-pointer`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={t('fileEditor.addFiles', 'Add files')}
|
||||
onClick={handleCardClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleCardClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Header bar - matches FileEditorThumbnail structure */}
|
||||
<div className={`${styles.header} ${styles.addFileHeader}`}>
|
||||
<div className={styles.logoMark}>
|
||||
<AddIcon sx={{ color: 'inherit', fontSize: '1.5rem' }} />
|
||||
</div>
|
||||
<div className={styles.headerIndex}>
|
||||
{t('fileEditor.addFiles', 'Add Files')}
|
||||
</div>
|
||||
<div className={styles.kebab} />
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className={styles.addFileContent}>
|
||||
{/* Stirling PDF Branding */}
|
||||
<Group gap="xs" align="center">
|
||||
<img
|
||||
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
|
||||
alt="Stirling PDF"
|
||||
style={{ height: '2.2rem', width: 'auto' }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Add Files + Native Upload Buttons - styled like LandingPage */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.6rem',
|
||||
width: '100%',
|
||||
marginTop: '0.8rem',
|
||||
marginBottom: '0.8rem'
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '2rem',
|
||||
height: '38px',
|
||||
paddingLeft: isUploadHover ? 0 : '1rem',
|
||||
paddingRight: isUploadHover ? 0 : '1rem',
|
||||
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
|
||||
minWidth: isUploadHover ? '58px' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && (
|
||||
<span>
|
||||
{t('landing.addFiles', 'Add Files')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--landing-button-bg)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? 'calc(100% - 58px - 0.6rem)' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease'
|
||||
}}
|
||||
onClick={handleNativeUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: '.8rem', textAlign: 'center', marginTop: '0.5rem' }}
|
||||
>
|
||||
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFileCard;
|
||||
@@ -34,9 +34,9 @@
|
||||
.header {
|
||||
height: 2.25rem;
|
||||
border-radius: 0.0625rem 0.0625rem 0 0;
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
user-select: none;
|
||||
background: var(--bg-toolbar);
|
||||
@@ -86,14 +86,23 @@
|
||||
}
|
||||
|
||||
.headerIndex {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.kebab {
|
||||
justify-self: end;
|
||||
.headerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.headerIconButton {
|
||||
color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
@@ -216,6 +225,11 @@
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.pinned {
|
||||
color: #FFC107 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Unsupported file indicator */
|
||||
.unsupportedPill {
|
||||
margin-left: 1.75rem;
|
||||
@@ -304,4 +318,84 @@
|
||||
/* Light mode selected header stroke override */
|
||||
:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] {
|
||||
outline-color: #3B4B6E;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Add File Card Styles
|
||||
========================= */
|
||||
|
||||
.addFileCard {
|
||||
background: var(--file-card-bg);
|
||||
border: 2px dashed var(--border-default);
|
||||
border-radius: 0.0625rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.addFileCard:hover {
|
||||
opacity: 1;
|
||||
border-color: var(--color-blue-500);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.addFileCard:focus {
|
||||
outline: 2px solid var(--color-blue-500);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.addFileHeader {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileHeader {
|
||||
background: var(--color-blue-500);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.addFileContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.addFileIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-subtle);
|
||||
transition: background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileIcon {
|
||||
background: var(--color-blue-50);
|
||||
}
|
||||
|
||||
.addFileText {
|
||||
font-weight: 500;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.addFileCard:hover .addFileText {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.addFileSubtext {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
Text, Center, Box, LoadingOverlay, Stack, Group
|
||||
} from '@mantine/core';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext';
|
||||
import { useFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext';
|
||||
import { useNavigationActions } from '../../contexts/NavigationContext';
|
||||
import { zipFileService } from '../../services/zipFileService';
|
||||
import { detectFileExtension } from '../../utils/fileUtils';
|
||||
import FileEditorThumbnail from './FileEditorThumbnail';
|
||||
import AddFileCard from './AddFileCard';
|
||||
import FilePickerModal from '../shared/FilePickerModal';
|
||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
||||
import { FileId, StirlingFile } from '../../types/fileContext';
|
||||
@@ -36,6 +37,7 @@ const FileEditor = ({
|
||||
// Use optimized FileContext hooks
|
||||
const { state, selectors } = useFileState();
|
||||
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
// Extract needed values from state (memoized to prevent infinite loops)
|
||||
const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]);
|
||||
@@ -171,8 +173,8 @@ const FileEditor = ({
|
||||
|
||||
// Process all extracted files
|
||||
if (allExtractedFiles.length > 0) {
|
||||
// Add files to context (they will be processed automatically)
|
||||
await addFiles(allExtractedFiles);
|
||||
// Add files to context and select them automatically
|
||||
await addFiles(allExtractedFiles, { selectFiles: true });
|
||||
showStatus(`Added ${allExtractedFiles.length} files`, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -286,7 +288,7 @@ const FileEditor = ({
|
||||
|
||||
|
||||
// File operations using context
|
||||
const handleDeleteFile = useCallback((fileId: FileId) => {
|
||||
const handleCloseFile = useCallback((fileId: FileId) => {
|
||||
const record = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||
const file = record ? selectors.getFile(record.id) : null;
|
||||
if (record && file) {
|
||||
@@ -308,6 +310,48 @@ const FileEditor = ({
|
||||
}
|
||||
}, [activeStirlingFileStubs, selectors, _setStatus]);
|
||||
|
||||
const handleUnzipFile = useCallback(async (fileId: FileId) => {
|
||||
const record = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||
const file = record ? selectors.getFile(record.id) : null;
|
||||
if (record && file) {
|
||||
try {
|
||||
// Extract and store files using shared service method
|
||||
const result = await zipFileService.extractAndStoreFilesWithHistory(file, record);
|
||||
|
||||
if (result.success && result.extractedStubs.length > 0) {
|
||||
// Add extracted file stubs to FileContext
|
||||
await actions.addStirlingFileStubs(result.extractedStubs);
|
||||
|
||||
// Remove the original ZIP file
|
||||
removeFiles([fileId], false);
|
||||
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: `Extracted ${result.extractedStubs.length} file(s) from ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3500
|
||||
});
|
||||
} else {
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: `Failed to extract files from ${file.name}`,
|
||||
body: result.errors.join('\n'),
|
||||
expandable: true,
|
||||
durationMs: 3500
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to unzip file:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: `Error unzipping ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3500
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [activeStirlingFileStubs, selectors, actions, removeFiles]);
|
||||
|
||||
const handleViewFile = useCallback((fileId: FileId) => {
|
||||
const record = activeStirlingFileStubs.find(r => r.id === fileId);
|
||||
if (record) {
|
||||
@@ -347,7 +391,7 @@ const FileEditor = ({
|
||||
<Box pos="relative" style={{ overflow: 'auto' }}>
|
||||
<LoadingOverlay visible={false} />
|
||||
|
||||
<Box p="md" pt="xl">
|
||||
<Box p="md">
|
||||
|
||||
|
||||
{activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
|
||||
@@ -405,6 +449,14 @@ const FileEditor = ({
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
>
|
||||
{/* Add File Card - only show when files exist */}
|
||||
{activeStirlingFileStubs.length > 0 && (
|
||||
<AddFileCard
|
||||
key="add-file-card"
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStirlingFileStubs.map((record, index) => {
|
||||
return (
|
||||
<FileEditorThumbnail
|
||||
@@ -415,11 +467,12 @@ const FileEditor = ({
|
||||
selectedFiles={localSelectedIds}
|
||||
selectionMode={selectionMode}
|
||||
onToggleFile={toggleFile}
|
||||
onDeleteFile={handleDeleteFile}
|
||||
onCloseFile={handleCloseFile}
|
||||
onViewFile={handleViewFile}
|
||||
_onSetStatus={showStatus}
|
||||
onReorderFiles={handleReorderFiles}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
onUnzipFile={handleUnzipFile}
|
||||
toolMode={toolMode}
|
||||
isSupported={isFileSupported(record.name)}
|
||||
/>
|
||||
@@ -437,7 +490,7 @@ const FileEditor = ({
|
||||
onSelectFiles={handleLoadFromStorage}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</Box>
|
||||
</Dropzone>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
||||
import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core';
|
||||
import { Text, ActionIcon, CheckboxIndicator, Tooltip } from '@mantine/core';
|
||||
import { alert } from '../toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import UnarchiveIcon from '@mui/icons-material/Unarchive';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { StirlingFileStub } from '../../types/fileContext';
|
||||
import { zipFileService } from '../../services/zipFileService';
|
||||
|
||||
import styles from './FileEditor.module.css';
|
||||
import { useFileContext } from '../../contexts/FileContext';
|
||||
@@ -27,11 +29,12 @@ interface FileEditorThumbnailProps {
|
||||
selectedFiles: FileId[];
|
||||
selectionMode: boolean;
|
||||
onToggleFile: (fileId: FileId) => void;
|
||||
onDeleteFile: (fileId: FileId) => void;
|
||||
onCloseFile: (fileId: FileId) => void;
|
||||
onViewFile: (fileId: FileId) => void;
|
||||
_onSetStatus: (status: string) => void;
|
||||
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
|
||||
onDownloadFile: (fileId: FileId) => void;
|
||||
onUnzipFile?: (fileId: FileId) => void;
|
||||
toolMode?: boolean;
|
||||
isSupported?: boolean;
|
||||
}
|
||||
@@ -41,10 +44,11 @@ const FileEditorThumbnail = ({
|
||||
index,
|
||||
selectedFiles,
|
||||
onToggleFile,
|
||||
onDeleteFile,
|
||||
onCloseFile,
|
||||
_onSetStatus,
|
||||
onReorderFiles,
|
||||
onDownloadFile,
|
||||
onUnzipFile,
|
||||
isSupported = true,
|
||||
}: FileEditorThumbnailProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -64,6 +68,9 @@ const FileEditorThumbnail = ({
|
||||
}, [activeFiles, file.id]);
|
||||
const isPinned = actualFile ? isFilePinned(actualFile) : false;
|
||||
|
||||
// Check if this is a ZIP file
|
||||
const isZipFile = zipFileService.isZipFileStub(file);
|
||||
|
||||
const pageCount = file.processedFile?.totalPages || 0;
|
||||
|
||||
const handleRef = useRef<HTMLSpanElement | null>(null);
|
||||
@@ -251,18 +258,60 @@ const FileEditorThumbnail = ({
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
{/* Kebab menu */}
|
||||
<ActionIcon
|
||||
aria-label={t('moreOptions', 'More options')}
|
||||
variant="subtle"
|
||||
className={styles.kebab}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowActions((v) => !v);
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
{/* Action buttons group */}
|
||||
<div className={styles.headerActions}>
|
||||
{/* Pin/Unpin icon */}
|
||||
<Tooltip label={isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}>
|
||||
<ActionIcon
|
||||
aria-label={isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}
|
||||
variant="subtle"
|
||||
className={isPinned ? styles.pinned : styles.headerIconButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (actualFile) {
|
||||
if (isPinned) {
|
||||
unpinFile(actualFile);
|
||||
alert({ alertType: 'neutral', title: `Unpinned ${file.name}`, expandable: false, durationMs: 3000 });
|
||||
} else {
|
||||
pinFile(actualFile);
|
||||
alert({ alertType: 'success', title: `Pinned ${file.name}`, expandable: false, durationMs: 3000 });
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isPinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Download icon */}
|
||||
<Tooltip label={t('download', 'Download')}>
|
||||
<ActionIcon
|
||||
aria-label={t('download', 'Download')}
|
||||
variant="subtle"
|
||||
className={styles.headerIconButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownloadFile(file.id);
|
||||
alert({ alertType: 'success', title: `Downloading ${file.name}`, expandable: false, durationMs: 2500 });
|
||||
}}
|
||||
>
|
||||
<DownloadOutlinedIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Kebab menu */}
|
||||
<ActionIcon
|
||||
aria-label={t('moreOptions', 'More options')}
|
||||
variant="subtle"
|
||||
className={styles.headerIconButton}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowActions((v) => !v);
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions overlay */}
|
||||
@@ -287,7 +336,7 @@ const FileEditorThumbnail = ({
|
||||
setShowActions(false);
|
||||
}}
|
||||
>
|
||||
{isPinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
{isPinned ? <PushPinIcon className={styles.pinned} fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
<span>{isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}</span>
|
||||
</button>
|
||||
|
||||
@@ -299,18 +348,28 @@ const FileEditorThumbnail = ({
|
||||
<span>{t('download', 'Download')}</span>
|
||||
</button>
|
||||
|
||||
{isZipFile && onUnzipFile && (
|
||||
<button
|
||||
className={styles.actionRow}
|
||||
onClick={() => { onUnzipFile(file.id); alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); setShowActions(false); }}
|
||||
>
|
||||
<UnarchiveIcon fontSize="small" />
|
||||
<span>{t('fileManager.unzip', 'Unzip')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={styles.actionsDivider} />
|
||||
|
||||
<button
|
||||
className={`${styles.actionRow} ${styles.actionDanger}`}
|
||||
onClick={() => {
|
||||
onDeleteFile(file.id);
|
||||
alert({ alertType: 'neutral', title: `Deleted ${file.name}`, expandable: false, durationMs: 3500 });
|
||||
onCloseFile(file.id);
|
||||
alert({ alertType: 'neutral', title: `Closed ${file.name}`, expandable: false, durationMs: 3500 });
|
||||
setShowActions(false);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
<span>{t('delete', 'Delete')}</span>
|
||||
<CloseIcon fontSize="small" />
|
||||
<span>{t('close', 'Close')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -324,7 +383,7 @@ const FileEditorThumbnail = ({
|
||||
marginTop: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}>
|
||||
<Text size="lg" fw={700} className={styles.title} lineClamp={2}>
|
||||
<Text size="lg" fw={700} className={`${styles.title} ph-no-capture `} lineClamp={2}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text
|
||||
@@ -350,6 +409,7 @@ const FileEditorThumbnail = ({
|
||||
<div className={styles.previewPaper}>
|
||||
{file.thumbnailUrl && (
|
||||
<img
|
||||
className="ph-no-capture"
|
||||
src={file.thumbnailUrl}
|
||||
alt={file.name}
|
||||
draggable={false}
|
||||
@@ -376,13 +436,6 @@ const FileEditorThumbnail = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pin indicator (bottom-left) */}
|
||||
{isPinned && (
|
||||
<span className={styles.pinIndicator} aria-hidden>
|
||||
<PushPinIcon fontSize="small" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Drag handle (span wrapper so we can attach a ref reliably) */}
|
||||
<span ref={handleRef} className={styles.dragHandle} aria-hidden>
|
||||
<DragIndicatorIcon fontSize="small" />
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
import LocalIcon from '../shared/LocalIcon';
|
||||
import { BASE_PATH } from '../../constants/app';
|
||||
|
||||
const EmptyFilesState: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { onLocalFileClick } = useFileManagerContext();
|
||||
const [isUploadHover, setIsUploadHover] = useState(false);
|
||||
|
||||
const handleUploadClick = () => {
|
||||
onLocalFileClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2rem'
|
||||
}}
|
||||
>
|
||||
{/* Container */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: '3rem 2rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '1.5rem',
|
||||
minWidth: '20rem',
|
||||
maxWidth: '28rem',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
{/* No Recent Files Message */}
|
||||
<Stack align="center" gap="sm">
|
||||
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
|
||||
<Text c="dimmed" ta="center" size="lg">
|
||||
{t('fileManager.noRecentFiles', 'No recent files')}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Stirling PDF Logo */}
|
||||
<Group gap="xs" align="center">
|
||||
<img
|
||||
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
|
||||
alt="Stirling PDF"
|
||||
style={{ height: '2.2rem', width: 'auto' }}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{/* Upload Button */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
marginTop: '0.5rem',
|
||||
marginBottom: '0.5rem'
|
||||
}}
|
||||
onMouseLeave={() => setIsUploadHover(false)}
|
||||
>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-file-manager)',
|
||||
color: 'var(--landing-button-color)',
|
||||
border: '1px solid var(--landing-button-border)',
|
||||
borderRadius: isUploadHover ? '2rem' : '1rem',
|
||||
height: '38px',
|
||||
width: isUploadHover ? '100%' : '58px',
|
||||
minWidth: '58px',
|
||||
paddingLeft: isUploadHover ? '1rem' : 0,
|
||||
paddingRight: isUploadHover ? '1rem' : 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'width .5s ease, padding .5s ease, border-radius .5s ease'
|
||||
}}
|
||||
onClick={handleUploadClick}
|
||||
onMouseEnter={() => setIsUploadHover(true)}
|
||||
>
|
||||
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: '.5rem' }}>
|
||||
{t('landing.uploadFromComputer', 'Upload from computer')}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: '.8rem', textAlign: 'center' }}
|
||||
>
|
||||
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyFilesState;
|
||||
@@ -26,7 +26,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
<ScrollArea style={{ flex: 1 }} p="md">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" py="xs">
|
||||
<Text size="sm" c="dimmed">{t('fileManager.fileName', 'Name')}</Text>
|
||||
<Text className='ph-no-capture' size="sm" c="dimmed">{t('fileManager.fileName', 'Name')}</Text>
|
||||
<Text size="sm" fw={500} style={{ maxWidth: '60%', textAlign: 'right' }} truncate>
|
||||
{currentFile ? currentFile.name : ''}
|
||||
</Text>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Center, ScrollArea, Text, Stack } from '@mantine/core';
|
||||
import CloudIcon from '@mui/icons-material/Cloud';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FileListItem from './FileListItem';
|
||||
import FileHistoryGroup from './FileHistoryGroup';
|
||||
import EmptyFilesState from './EmptyFilesState';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
|
||||
interface FileListAreaProps {
|
||||
@@ -29,6 +29,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
||||
onFileDoubleClick,
|
||||
onDownloadSingle,
|
||||
isFileSupported,
|
||||
isLoading,
|
||||
} = useFileManagerContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -43,15 +44,11 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
||||
scrollbarSize={8}
|
||||
>
|
||||
<Stack gap={0}>
|
||||
{recentFiles.length === 0 ? (
|
||||
{recentFiles.length === 0 && !isLoading ? (
|
||||
<EmptyFilesState />
|
||||
) : recentFiles.length === 0 && isLoading ? (
|
||||
<Center style={{ height: '12.5rem' }}>
|
||||
<Stack align="center" gap="sm">
|
||||
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
|
||||
<Text c="dimmed" ta="center">{t('fileManager.noRecentFiles', 'No recent files')}</Text>
|
||||
<Text size="xs" c="dimmed" ta="center" style={{ opacity: 0.7 }}>
|
||||
{t('fileManager.dropFilesHint', 'Drop files anywhere to upload')}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text c="dimmed" ta="center">{t('fileManager.loadingFiles', 'Loading files...')}</Text>
|
||||
</Center>
|
||||
) : (
|
||||
filteredFiles.map((file, index) => {
|
||||
|
||||
@@ -5,10 +5,12 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import RestoreIcon from '@mui/icons-material/Restore';
|
||||
import UnarchiveIcon from '@mui/icons-material/Unarchive';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
||||
import { FileId, StirlingFileStub } from '../../types/fileContext';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
import { zipFileService } from '../../services/zipFileService';
|
||||
import ToolChain from '../shared/ToolChain';
|
||||
|
||||
interface FileListItemProps {
|
||||
@@ -38,7 +40,10 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const {expandedFileIds, onToggleExpansion, onAddToRecents } = useFileManagerContext();
|
||||
const {expandedFileIds, onToggleExpansion, onAddToRecents, onUnzipFile } = useFileManagerContext();
|
||||
|
||||
// Check if this is a ZIP file
|
||||
const isZipFile = zipFileService.isZipFileStub(file);
|
||||
|
||||
// Keep item in hovered state if menu is open
|
||||
const shouldShowHovered = isHovered || isMenuOpen;
|
||||
@@ -93,7 +98,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>{file.name}</Text>
|
||||
<Text size="sm" fw={500} className='ph-no-capture' truncate style={{ flex: 1 }}>{file.name}</Text>
|
||||
<Badge size="xs" variant="light" color={"blue"}>
|
||||
v{currentVersion}
|
||||
</Badge>
|
||||
@@ -192,6 +197,22 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Unzip option for ZIP files */}
|
||||
{isZipFile && !isHistoryFile && (
|
||||
<>
|
||||
<Menu.Item
|
||||
leftSection={<UnarchiveIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnzipFile(file);
|
||||
}}
|
||||
>
|
||||
{t('fileManager.unzip', 'Unzip')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import UploadIcon from '@mui/icons-material/Upload';
|
||||
import CloudIcon from '@mui/icons-material/Cloud';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||
import { useGoogleDrivePicker } from '../../hooks/useGoogleDrivePicker';
|
||||
|
||||
interface FileSourceButtonsProps {
|
||||
horizontal?: boolean;
|
||||
@@ -13,8 +14,20 @@ interface FileSourceButtonsProps {
|
||||
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||
horizontal = false
|
||||
}) => {
|
||||
const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext();
|
||||
const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect } = useFileManagerContext();
|
||||
const { t } = useTranslation();
|
||||
const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker();
|
||||
|
||||
const handleGoogleDriveClick = async () => {
|
||||
try {
|
||||
const files = await openGoogleDrivePicker({ multiple: true });
|
||||
if (files.length > 0) {
|
||||
onGoogleDriveSelect(files);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to pick files from Google Drive:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonProps = {
|
||||
variant: (source: string) => activeSource === source ? 'filled' : 'subtle',
|
||||
@@ -67,15 +80,24 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={buttonProps.variant('drive')}
|
||||
variant="subtle"
|
||||
color='var(--mantine-color-gray-6)'
|
||||
leftSection={<CloudIcon />}
|
||||
justify={horizontal ? "center" : "flex-start"}
|
||||
onClick={() => onSourceChange('drive')}
|
||||
onClick={handleGoogleDriveClick}
|
||||
fullWidth={!horizontal}
|
||||
size={horizontal ? "xs" : "sm"}
|
||||
disabled
|
||||
color={activeSource === 'drive' ? 'gray' : undefined}
|
||||
styles={buttonProps.getStyles('drive')}
|
||||
disabled={!isGoogleDriveEnabled}
|
||||
styles={{
|
||||
root: {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
'&:hover': {
|
||||
backgroundColor: isGoogleDriveEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={!isGoogleDriveEnabled ? t('fileManager.googleDriveNotAvailable', 'Google Drive integration not available') : undefined}
|
||||
>
|
||||
{horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')}
|
||||
</Button>
|
||||
|
||||
@@ -9,7 +9,6 @@ const HiddenFileInput: React.FC = () => {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple={true}
|
||||
accept={["*/*"] as any}
|
||||
onChange={onFileInputChange}
|
||||
style={{ display: 'none' }}
|
||||
data-testid="file-input"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { HotkeyBinding } from '../../utils/hotkeys';
|
||||
import { useHotkeys } from '../../contexts/HotkeyContext';
|
||||
|
||||
interface HotkeyDisplayProps {
|
||||
binding: HotkeyBinding | null | undefined;
|
||||
size?: 'sm' | 'md';
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
const baseKeyStyle: React.CSSProperties = {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '0.375rem',
|
||||
background: 'var(--mantine-color-gray-1)',
|
||||
border: '1px solid var(--mantine-color-gray-3)',
|
||||
padding: '0.125rem 0.35rem',
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: 1,
|
||||
fontFamily: 'var(--mantine-font-family-monospace, monospace)',
|
||||
minWidth: '1.35rem',
|
||||
color: 'var(--mantine-color-text)',
|
||||
};
|
||||
|
||||
export const HotkeyDisplay: React.FC<HotkeyDisplayProps> = ({ binding, size = 'sm', muted = false }) => {
|
||||
const { getDisplayParts } = useHotkeys();
|
||||
const parts = getDisplayParts(binding);
|
||||
|
||||
if (!binding || parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyStyle = size === 'md'
|
||||
? { ...baseKeyStyle, fontSize: '0.85rem', padding: '0.2rem 0.5rem' }
|
||||
: baseKeyStyle;
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.25rem',
|
||||
color: muted ? 'var(--mantine-color-dimmed)' : 'inherit',
|
||||
fontWeight: muted ? 500 : 600,
|
||||
}}
|
||||
>
|
||||
{parts.map((part, index) => (
|
||||
<React.Fragment key={`${part}-${index}`}>
|
||||
<kbd style={keyStyle}>{part}</kbd>
|
||||
{index < parts.length - 1 && <span aria-hidden style={{ fontWeight: 400 }}>+</span>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotkeyDisplay;
|
||||
@@ -146,11 +146,12 @@ export default function Workbench() {
|
||||
}
|
||||
>
|
||||
{/* Top Controls */}
|
||||
<TopControls
|
||||
currentView={currentView}
|
||||
setCurrentView={setCurrentView}
|
||||
selectedToolKey={selectedToolId}
|
||||
/>
|
||||
{activeFiles.length > 0 && (
|
||||
<TopControls
|
||||
currentView={currentView}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Dismiss All Errors Button */}
|
||||
<DismissAllErrorsButton />
|
||||
@@ -160,6 +161,7 @@ export default function Workbench() {
|
||||
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
|
||||
style={{
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
paddingTop: activeFiles.length > 0 ? '3.5rem' : '0',
|
||||
}}
|
||||
>
|
||||
{renderMainContent()}
|
||||
|
||||
@@ -317,6 +317,7 @@ const FileThumbnail = ({
|
||||
>
|
||||
{file.thumbnail && (
|
||||
<img
|
||||
className="ph-no-capture"
|
||||
src={file.thumbnail}
|
||||
alt={file.name}
|
||||
draggable={false}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||
import { pdfExportService } from "../../services/pdfExportService";
|
||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||
import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
|
||||
import { createStirlingFilesAndStubs } from "../../services/fileStubHelpers";
|
||||
// Thumbnail generation is now handled by individual PageThumbnail components
|
||||
import './PageEditor.module.css';
|
||||
import PageThumbnail from './PageThumbnail';
|
||||
@@ -36,6 +39,9 @@ const PageEditor = ({
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
// Navigation guard for unsaved changes
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
// Prefer IDs + selectors to avoid array identity churn
|
||||
const activeFileIds = state.files.ids;
|
||||
|
||||
@@ -82,6 +88,12 @@ const PageEditor = ({
|
||||
updateUndoRedoState();
|
||||
}, [updateUndoRedoState]);
|
||||
|
||||
// Wrapper for executeCommand to track unsaved changes
|
||||
const executeCommandWithTracking = useCallback((command: any) => {
|
||||
undoManagerRef.current.executeCommand(command);
|
||||
setHasUnsavedChanges(true);
|
||||
}, [setHasUnsavedChanges]);
|
||||
|
||||
// Watch for container size changes to update split line positions
|
||||
useEffect(() => {
|
||||
const container = gridContainerRef.current;
|
||||
@@ -138,17 +150,16 @@ const PageEditor = ({
|
||||
// DOM-first command handlers
|
||||
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||
}, []);
|
||||
executeCommandWithTracking(bulkRotateCommand);
|
||||
}, [executeCommandWithTracking]);
|
||||
|
||||
// Command factory functions for PageThumbnail
|
||||
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
|
||||
execute: () => {
|
||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||
|
||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||
executeCommandWithTracking(bulkRotateCommand);
|
||||
}
|
||||
}), []);
|
||||
}), [executeCommandWithTracking]);
|
||||
|
||||
const createDeleteCommand = useCallback((pageIds: string[]) => ({
|
||||
execute: () => {
|
||||
@@ -174,10 +185,10 @@ const PageEditor = ({
|
||||
() => getPageNumbersFromIds(selectedPageIds),
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}
|
||||
}
|
||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const createSplitCommand = useCallback((position: number) => ({
|
||||
execute: () => {
|
||||
@@ -186,9 +197,9 @@ const PageEditor = ({
|
||||
() => splitPositions,
|
||||
setSplitPositions
|
||||
);
|
||||
undoManagerRef.current.executeCommand(splitCommand);
|
||||
executeCommandWithTracking(splitCommand);
|
||||
}
|
||||
}), [splitPositions]);
|
||||
}), [splitPositions, executeCommandWithTracking]);
|
||||
|
||||
// Command executor for PageThumbnail
|
||||
const executeCommand = useCallback((command: any) => {
|
||||
@@ -232,8 +243,8 @@ const PageEditor = ({
|
||||
() => selectedPageNumbers,
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]);
|
||||
|
||||
const handleDeletePage = useCallback((pageNumber: number) => {
|
||||
if (!displayDocument) return;
|
||||
@@ -251,8 +262,8 @@ const PageEditor = ({
|
||||
() => getPageNumbersFromIds(selectedPageIds),
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleSplit = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -298,8 +309,8 @@ const PageEditor = ({
|
||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleSplitAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -344,8 +355,8 @@ const PageEditor = ({
|
||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handlePageBreak = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -358,8 +369,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handlePageBreakAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -372,8 +383,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
||||
if (!displayDocument || files.length === 0) return;
|
||||
@@ -416,8 +427,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(reorderCommand);
|
||||
}, [displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(reorderCommand);
|
||||
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
// Helper function to collect source files for multi-file export
|
||||
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
||||
@@ -499,13 +510,14 @@ const PageEditor = ({
|
||||
|
||||
// Step 4: Download the result
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||
|
||||
setExportLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||
|
||||
const onExportAll = useCallback(async () => {
|
||||
if (!displayDocument) return;
|
||||
@@ -514,87 +526,79 @@ const PageEditor = ({
|
||||
try {
|
||||
// Step 1: Apply DOM changes to document state first
|
||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||
mergedPdfDocument || displayDocument, // Original order
|
||||
displayDocument, // Current display order (includes reordering)
|
||||
splitPositions // Position-based splits
|
||||
mergedPdfDocument || displayDocument,
|
||||
displayDocument,
|
||||
splitPositions
|
||||
);
|
||||
|
||||
// Step 2: Check if we have multiple documents (splits) or single document
|
||||
if (Array.isArray(processedDocuments)) {
|
||||
// Multiple documents (splits) - export as ZIP
|
||||
const blobs: Blob[] = [];
|
||||
const filenames: string[] = [];
|
||||
// Step 2: Export to files
|
||||
const sourceFiles = getSourceFiles();
|
||||
const exportFilename = getExportFilename();
|
||||
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
|
||||
|
||||
const sourceFiles = getSourceFiles();
|
||||
const baseExportFilename = getExportFilename();
|
||||
const baseName = baseExportFilename.replace(/\.pdf$/i, '');
|
||||
|
||||
for (let i = 0; i < processedDocuments.length; i++) {
|
||||
const doc = processedDocuments[i];
|
||||
const partFilename = `${baseName}_part_${i + 1}.pdf`;
|
||||
|
||||
const result = sourceFiles
|
||||
? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename })
|
||||
: await pdfExportService.exportPDF(doc, [], { filename: partFilename });
|
||||
blobs.push(result.blob);
|
||||
filenames.push(result.filename);
|
||||
}
|
||||
|
||||
// Create ZIP file
|
||||
// Step 3: Download
|
||||
if (files.length > 1) {
|
||||
// Multiple files - create ZIP
|
||||
const JSZip = await import('jszip');
|
||||
const zip = new JSZip.default();
|
||||
|
||||
blobs.forEach((blob, index) => {
|
||||
zip.file(filenames[index], blob);
|
||||
files.forEach((file) => {
|
||||
zip.file(file.name, file);
|
||||
});
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
|
||||
const exportFilename = getExportFilename();
|
||||
const zipFilename = exportFilename.replace(/\.pdf$/i, '.zip');
|
||||
|
||||
pdfExportService.downloadFile(zipBlob, zipFilename);
|
||||
} else {
|
||||
// Single document - regular export
|
||||
const sourceFiles = getSourceFiles();
|
||||
const exportFilename = getExportFilename();
|
||||
const result = sourceFiles
|
||||
? await pdfExportService.exportPDFMultiFile(
|
||||
processedDocuments,
|
||||
sourceFiles,
|
||||
[],
|
||||
{ selectedOnly: false, filename: exportFilename }
|
||||
)
|
||||
: await pdfExportService.exportPDF(
|
||||
processedDocuments,
|
||||
[],
|
||||
{ selectedOnly: false, filename: exportFilename }
|
||||
);
|
||||
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
// Single file - download directly
|
||||
const file = files[0];
|
||||
pdfExportService.downloadFile(file, file.name);
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
setExportLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||
|
||||
// Apply DOM changes to document state using dedicated service
|
||||
const applyChanges = useCallback(() => {
|
||||
const applyChanges = useCallback(async () => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
// Pass current display document (which includes reordering) to get both reordering AND DOM changes
|
||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||
mergedPdfDocument || displayDocument, // Original order
|
||||
displayDocument, // Current display order (includes reordering)
|
||||
splitPositions // Position-based splits
|
||||
);
|
||||
setExportLoading(true);
|
||||
try {
|
||||
// Step 1: Apply DOM changes to document state first
|
||||
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||
mergedPdfDocument || displayDocument,
|
||||
displayDocument,
|
||||
splitPositions
|
||||
);
|
||||
|
||||
// For apply changes, we only set the first document if it's an array (splits shouldn't affect document state)
|
||||
const documentToSet = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments;
|
||||
setEditedDocument(documentToSet);
|
||||
// Step 2: Export to files
|
||||
const sourceFiles = getSourceFiles();
|
||||
const exportFilename = getExportFilename();
|
||||
const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
|
||||
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions]);
|
||||
// Step 3: Create StirlingFiles and stubs for version history
|
||||
const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
|
||||
if (!parentStub) throw new Error('Parent stub not found');
|
||||
|
||||
const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool');
|
||||
|
||||
// Step 4: Consume files (replace in context)
|
||||
await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
|
||||
|
||||
setHasUnsavedChanges(false);
|
||||
setExportLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Apply changes failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
|
||||
|
||||
|
||||
const closePdf = useCallback(() => {
|
||||
@@ -658,7 +662,7 @@ const PageEditor = ({
|
||||
const displayedPages = displayDocument?.pages || [];
|
||||
|
||||
return (
|
||||
<Box pos="relative" h='100%' pt={40} style={{ overflow: 'auto' }} data-scrolling-container="true">
|
||||
<Box pos="relative" h='100%' style={{ overflow: 'auto' }} data-scrolling-container="true">
|
||||
<LoadingOverlay visible={globalProcessing && !mergedPdfDocument} />
|
||||
|
||||
{!mergedPdfDocument && !globalProcessing && activeFileIds.length === 0 && (
|
||||
@@ -779,7 +783,14 @@ const PageEditor = ({
|
||||
)}
|
||||
|
||||
|
||||
<NavigationWarningModal />
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={async () => {
|
||||
await applyChanges();
|
||||
}}
|
||||
onExportAndContinue={async () => {
|
||||
await onExportAll();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -371,9 +371,11 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
</div>
|
||||
) : thumbnailUrl ? (
|
||||
<img
|
||||
className="ph-no-capture"
|
||||
src={thumbnailUrl}
|
||||
alt={`Page ${page.pageNumber}`}
|
||||
draggable={false}
|
||||
data-original-rotation={page.rotation}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
@@ -17,32 +17,34 @@ export class RotatePageCommand extends DOMCommand {
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
// Only update DOM for immediate visual feedback
|
||||
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||
if (pageElement) {
|
||||
const img = pageElement.querySelector('img');
|
||||
if (img) {
|
||||
// Extract current rotation from transform property to match the animated CSS
|
||||
const currentTransform = img.style.transform || '';
|
||||
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||
const newRotation = currentRotation + this.degrees;
|
||||
let newRotation = currentRotation + this.degrees;
|
||||
|
||||
newRotation = ((newRotation % 360) + 360) % 360;
|
||||
|
||||
img.style.transform = `rotate(${newRotation}deg)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
// Only update DOM
|
||||
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||
if (pageElement) {
|
||||
const img = pageElement.querySelector('img');
|
||||
if (img) {
|
||||
// Extract current rotation from transform property
|
||||
const currentTransform = img.style.transform || '';
|
||||
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
|
||||
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
|
||||
const previousRotation = currentRotation - this.degrees;
|
||||
let previousRotation = currentRotation - this.degrees;
|
||||
|
||||
previousRotation = ((previousRotation % 360) + 360) % 360;
|
||||
|
||||
img.style.transform = `rotate(${previousRotation}deg)`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* AppConfigModal styles */
|
||||
.modal-container {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
height: 37.5rem; /* 600px */
|
||||
}
|
||||
|
||||
.modal-nav {
|
||||
width: 15rem; /* 240px */
|
||||
height: 37.5rem; /* 600px */
|
||||
border-top-left-radius: 0.75rem; /* 12px */
|
||||
border-bottom-left-radius: 0.75rem; /* 12px */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Mobile: compact icon-only navigation */
|
||||
@media (max-width: 1024px) {
|
||||
.modal-container {
|
||||
height: 100vh !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.modal-nav {
|
||||
width: 5rem; /* 80px - wider for larger icons */
|
||||
height: 100vh !important;
|
||||
max-height: none !important;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.modal-nav-scroll {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.modal-nav-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-nav-item.mobile {
|
||||
padding: 1rem;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
height: 100vh !important;
|
||||
max-height: none !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-nav-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.modal-nav-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-nav-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-nav-section-items {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-nav-item {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0.625rem; /* 8px 10px */
|
||||
border-radius: 0.5rem; /* 8px */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem; /* 4px */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
height: 37.5rem; /* 600px */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-content-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.modal-content-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.confirm-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.confirm-modal-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Modal, Button, Stack, Text, Code, ScrollArea, Group, Badge, Alert, Loader } from '@mantine/core';
|
||||
import { useAppConfig } from '../../hooks/useAppConfig';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { Modal, Text, ActionIcon } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import LocalIcon from './LocalIcon';
|
||||
import Overview from './config/configSections/Overview';
|
||||
import { createConfigNavSections } from './config/configNavSections';
|
||||
import { NavKey } from './config/types';
|
||||
import './AppConfigModal.css';
|
||||
|
||||
interface AppConfigModalProps {
|
||||
opened: boolean;
|
||||
@@ -8,131 +13,143 @@ interface AppConfigModalProps {
|
||||
}
|
||||
|
||||
const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
const { config, loading, error, refetch } = useAppConfig();
|
||||
const [active, setActive] = useState<NavKey>('overview');
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
const renderConfigSection = (title: string, data: any) => {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
useEffect(() => {
|
||||
const handler = (ev: Event) => {
|
||||
const detail = (ev as CustomEvent).detail as { key?: NavKey } | undefined;
|
||||
if (detail?.key) {
|
||||
setActive(detail.key);
|
||||
}
|
||||
};
|
||||
window.addEventListener('appConfig:navigate', handler as EventListener);
|
||||
return () => window.removeEventListener('appConfig:navigate', handler as EventListener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap="xs" mb="md">
|
||||
<Text fw={600} size="md" c="blue">{title}</Text>
|
||||
<Stack gap="xs" pl="md">
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<Group key={key} wrap="nowrap" align="flex-start">
|
||||
<Text size="sm" w={150} style={{ flexShrink: 0 }} c="dimmed">
|
||||
{key}:
|
||||
</Text>
|
||||
{typeof value === 'boolean' ? (
|
||||
<Badge color={value ? 'green' : 'red'} size="sm">
|
||||
{value ? 'true' : 'false'}
|
||||
</Badge>
|
||||
) : typeof value === 'object' ? (
|
||||
<Code block>{JSON.stringify(value, null, 2)}</Code>
|
||||
) : (
|
||||
String(value) || 'null'
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
const colors = useMemo(() => ({
|
||||
navBg: 'var(--modal-nav-bg)',
|
||||
sectionTitle: 'var(--modal-nav-section-title)',
|
||||
navItem: 'var(--modal-nav-item)',
|
||||
navItemActive: 'var(--modal-nav-item-active)',
|
||||
navItemActiveBg: 'var(--modal-nav-item-active-bg)',
|
||||
contentBg: 'var(--modal-content-bg)',
|
||||
headerBorder: 'var(--modal-header-border)',
|
||||
}), []);
|
||||
|
||||
// Placeholder logout handler (not needed in open-source but keeps SaaS compatibility)
|
||||
const handleLogout = () => {
|
||||
// In SaaS this would sign out, in open-source it does nothing
|
||||
console.log('Logout placeholder for SaaS compatibility');
|
||||
};
|
||||
|
||||
const basicConfig = config ? {
|
||||
appName: config.appName,
|
||||
appNameNavbar: config.appNameNavbar,
|
||||
baseUrl: config.baseUrl,
|
||||
contextPath: config.contextPath,
|
||||
serverPort: config.serverPort,
|
||||
} : null;
|
||||
// Left navigation structure and icons
|
||||
const configNavSections = useMemo(() =>
|
||||
createConfigNavSections(
|
||||
Overview,
|
||||
handleLogout
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const securityConfig = config ? {
|
||||
enableLogin: config.enableLogin,
|
||||
} : null;
|
||||
const activeLabel = useMemo(() => {
|
||||
for (const section of configNavSections) {
|
||||
const found = section.items.find(i => i.key === active);
|
||||
if (found) return found.label;
|
||||
}
|
||||
return '';
|
||||
}, [configNavSections, active]);
|
||||
|
||||
const systemConfig = config ? {
|
||||
enableAlphaFunctionality: config.enableAlphaFunctionality,
|
||||
enableAnalytics: config.enableAnalytics,
|
||||
} : null;
|
||||
|
||||
const premiumConfig = config ? {
|
||||
premiumEnabled: config.premiumEnabled,
|
||||
premiumKey: config.premiumKey ? '***hidden***' : null,
|
||||
runningProOrHigher: config.runningProOrHigher,
|
||||
runningEE: config.runningEE,
|
||||
license: config.license,
|
||||
} : null;
|
||||
|
||||
const integrationConfig = config ? {
|
||||
GoogleDriveEnabled: config.GoogleDriveEnabled,
|
||||
SSOAutoLogin: config.SSOAutoLogin,
|
||||
} : null;
|
||||
|
||||
const legalConfig = config ? {
|
||||
termsAndConditions: config.termsAndConditions,
|
||||
privacyPolicy: config.privacyPolicy,
|
||||
cookiePolicy: config.cookiePolicy,
|
||||
impressum: config.impressum,
|
||||
accessibilityStatement: config.accessibilityStatement,
|
||||
} : null;
|
||||
const activeComponent = useMemo(() => {
|
||||
for (const section of configNavSections) {
|
||||
const found = section.items.find(i => i.key === active);
|
||||
if (found) return found.component;
|
||||
}
|
||||
return null;
|
||||
}, [configNavSections, active]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="App Configuration (Testing)"
|
||||
size="lg"
|
||||
title={null}
|
||||
size={isMobile ? "100%" : 980}
|
||||
centered
|
||||
radius="lg"
|
||||
withCloseButton={false}
|
||||
style={{ zIndex: 1000 }}
|
||||
overlayProps={{ opacity: 0.35, blur: 2 }}
|
||||
padding={0}
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<Stack>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
This modal shows the current application configuration for testing purposes only.
|
||||
</Text>
|
||||
<Button size="xs" variant="light" onClick={refetch}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
<div className="modal-container">
|
||||
{/* Left navigation */}
|
||||
<div
|
||||
className={`modal-nav ${isMobile ? 'mobile' : ''}`}
|
||||
style={{
|
||||
background: colors.navBg,
|
||||
borderRight: `1px solid ${colors.headerBorder}`,
|
||||
}}
|
||||
>
|
||||
<div className="modal-nav-scroll">
|
||||
{configNavSections.map(section => (
|
||||
<div key={section.title} className="modal-nav-section">
|
||||
{!isMobile && (
|
||||
<Text size="xs" fw={600} c={colors.sectionTitle} style={{ textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||
{section.title}
|
||||
</Text>
|
||||
)}
|
||||
<div className="modal-nav-section-items">
|
||||
{section.items.map(item => {
|
||||
const isActive = active === item.key;
|
||||
const color = isActive ? colors.navItemActive : colors.navItem;
|
||||
const iconSize = isMobile ? 28 : 18;
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
onClick={() => setActive(item.key)}
|
||||
className={`modal-nav-item ${isMobile ? 'mobile' : ''}`}
|
||||
style={{
|
||||
background: isActive ? colors.navItemActiveBg : 'transparent',
|
||||
}}
|
||||
>
|
||||
<LocalIcon icon={item.icon} width={iconSize} height={iconSize} style={{ color }} />
|
||||
{!isMobile && (
|
||||
<Text size="sm" fw={500} style={{ color }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<Stack align="center" py="md">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">Loading configuration...</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert color="red" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{config && (
|
||||
<ScrollArea h={400}>
|
||||
<Stack gap="lg">
|
||||
{renderConfigSection('Basic Configuration', basicConfig)}
|
||||
{renderConfigSection('Security Configuration', securityConfig)}
|
||||
{renderConfigSection('System Configuration', systemConfig)}
|
||||
{renderConfigSection('Premium/Enterprise Configuration', premiumConfig)}
|
||||
{renderConfigSection('Integration Configuration', integrationConfig)}
|
||||
{renderConfigSection('Legal Configuration', legalConfig)}
|
||||
|
||||
{config.error && (
|
||||
<Alert color="yellow" title="Configuration Warning">
|
||||
{config.error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="md" c="blue">Raw Configuration</Text>
|
||||
<Code block style={{ fontSize: '11px' }}>
|
||||
{JSON.stringify(config, null, 2)}
|
||||
</Code>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</Stack>
|
||||
{/* Right content */}
|
||||
<div className="modal-content">
|
||||
<div className="modal-content-scroll">
|
||||
{/* Sticky header with section title and small close button */}
|
||||
<div
|
||||
className="modal-header"
|
||||
style={{
|
||||
background: colors.contentBg,
|
||||
borderBottom: `1px solid ${colors.headerBorder}`,
|
||||
}}
|
||||
>
|
||||
<Text fw={700} size="lg">{activeLabel}</Text>
|
||||
<ActionIcon variant="subtle" onClick={onClose} aria-label="Close">
|
||||
<LocalIcon icon="close-rounded" width={18} height={18} />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{activeComponent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Text,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { FileButton, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -15,7 +15,7 @@ interface FileUploadButtonProps {
|
||||
const FileUploadButton = ({
|
||||
file,
|
||||
onChange,
|
||||
accept = "*/*",
|
||||
accept,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
variant = "outline",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCookieConsent } from '../../hooks/useCookieConsent';
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ const LandingPage = () => {
|
||||
{/* White PDF Page Background */}
|
||||
<Dropzone
|
||||
onDrop={handleFileDrop}
|
||||
accept={["application/pdf", "application/zip", "application/x-zip-compressed"]}
|
||||
multiple={true}
|
||||
className="w-4/5 flex items-center justify-center h-[95%]"
|
||||
style={{
|
||||
@@ -178,7 +177,6 @@ const LandingPage = () => {
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.zip"
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
@@ -269,8 +269,9 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-
|
||||
<ScrollArea h={190} type="scroll">
|
||||
<div className={styles.languageGrid}>
|
||||
{languageOptions.map((option, index) => {
|
||||
const isEnglishGB = option.value === 'en-GB'; // Currently only English GB has enough translations to use
|
||||
const isDisabled = !isEnglishGB;
|
||||
// Enable languages with >90% translation completion
|
||||
const enabledLanguages = ['en-GB', 'ar-AR', 'de-DE', 'es-ES', 'fr-FR', 'it-IT', 'pt-BR', 'ru-RU', 'zh-CN'];
|
||||
const isDisabled = !enabledLanguages.includes(option.value);
|
||||
|
||||
return (
|
||||
<LanguageItem
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Box, Group, Text, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -37,7 +36,7 @@ const MultiSelectControls = ({
|
||||
>
|
||||
{t("fileManager.clearSelection", "Clear Selection")}
|
||||
</Button>
|
||||
|
||||
|
||||
{onAddToUpload && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -47,7 +46,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.addToUpload", "Add to Upload")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onOpenInFileEditor && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -58,7 +57,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.openInFileEditor", "Open in File Editor")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onOpenInPageEditor && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -69,7 +68,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.openInPageEditor", "Open in Page Editor")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onDeleteAll && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -85,4 +84,4 @@ const MultiSelectControls = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelectControls;
|
||||
export default MultiSelectControls;
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||
import { useNavigationGuard } from '../../contexts/NavigationContext';
|
||||
import { Modal, Text, Button, Group, Stack } from "@mantine/core";
|
||||
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
|
||||
|
||||
interface NavigationWarningModalProps {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
onExportAndContinue?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const NavigationWarningModal = ({
|
||||
onApplyAndContinue,
|
||||
onExportAndContinue
|
||||
}: NavigationWarningModalProps) => {
|
||||
const {
|
||||
showNavigationWarning,
|
||||
hasUnsavedChanges,
|
||||
cancelNavigation,
|
||||
confirmNavigation,
|
||||
setHasUnsavedChanges
|
||||
} = useNavigationGuard();
|
||||
const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { showNavigationWarning, hasUnsavedChanges, cancelNavigation, confirmNavigation, setHasUnsavedChanges } =
|
||||
useNavigationGuard();
|
||||
|
||||
const handleKeepWorking = () => {
|
||||
cancelNavigation();
|
||||
@@ -36,13 +32,14 @@ const NavigationWarningModal = ({
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleExportAndContinue = async () => {
|
||||
const _handleExportAndContinue = async () => {
|
||||
if (onExportAndContinue) {
|
||||
await onExportAndContinue();
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
confirmNavigation();
|
||||
};
|
||||
const BUTTON_WIDTH = "10rem";
|
||||
|
||||
if (!hasUnsavedChanges) {
|
||||
return null;
|
||||
@@ -52,55 +49,58 @@ const NavigationWarningModal = ({
|
||||
<Modal
|
||||
opened={showNavigationWarning}
|
||||
onClose={handleKeepWorking}
|
||||
title="Unsaved Changes"
|
||||
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
||||
centered
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
size="auto"
|
||||
closeOnClickOutside={true}
|
||||
closeOnEscape={true}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
You have unsaved changes to your PDF. What would you like to do?
|
||||
<Stack>
|
||||
<Stack ta="center" p="md">
|
||||
<Text size="md" fw="300">
|
||||
{t("unsavedChanges", "You have unsaved changes to your PDF.")}
|
||||
</Text>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={handleKeepWorking}
|
||||
>
|
||||
Keep Working
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={handleDiscardChanges}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
|
||||
{onApplyAndContinue && (
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={handleApplyAndContinue}
|
||||
>
|
||||
Apply & Continue
|
||||
<Text size="lg" fw="500" >
|
||||
{t("areYouSure", "Are you sure you want to leave?")}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Desktop layout: 2 groups side by side */}
|
||||
<Group justify="space-between" gap="xl" visibleFrom="md">
|
||||
<Group gap="sm">
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={handleKeepWorking} w={BUTTON_WIDTH} leftSection={<ArrowBackIcon fontSize="small" />}>
|
||||
{t("keepWorking", "Keep Working")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onExportAndContinue && (
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleExportAndContinue}
|
||||
>
|
||||
Export & Continue
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
</Button>
|
||||
)}
|
||||
{onApplyAndContinue && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Mobile layout: centered stack of 4 buttons */}
|
||||
<Stack align="center" gap="sm" hiddenFrom="md">
|
||||
<Button variant="light" color="var(--mantine-color-gray-8)" onClick={handleKeepWorking} w={BUTTON_WIDTH} leftSection={<ArrowBackIcon fontSize="small" />}>
|
||||
{t("keepWorking", "Keep Working")}
|
||||
</Button>
|
||||
<Button variant="filled" color="var(--mantine-color-red-9)" onClick={handleDiscardChanges} w={BUTTON_WIDTH} leftSection={<DeleteOutlineIcon fontSize="small" />}>
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
</Button>
|
||||
{onApplyAndContinue && (
|
||||
<Button variant="filled" onClick={handleApplyAndContinue} w={BUTTON_WIDTH} leftSection={<CheckCircleOutlineIcon fontSize="small" />}>
|
||||
{t("applyAndContinue", "Apply & Leave")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationWarningModal;
|
||||
export default NavigationWarningModal;
|
||||
|
||||
@@ -12,6 +12,8 @@ import { ButtonConfig } from '../../types/sidebar';
|
||||
import './quickAccessBar/QuickAccessBar.css';
|
||||
import AllToolsNavButton from './AllToolsNavButton';
|
||||
import ActiveToolButton from "./quickAccessBar/ActiveToolButton";
|
||||
import AppConfigModal from './AppConfigModal';
|
||||
import { useAppConfig } from '../../hooks/useAppConfig';
|
||||
import {
|
||||
isNavButtonActive,
|
||||
getNavButtonStyle,
|
||||
@@ -24,6 +26,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||
const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||
const { getToolNavigation } = useSidebarNavigation();
|
||||
const { config } = useAppConfig();
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [activeButton, setActiveButton] = useState<string>('tools');
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
@@ -41,10 +44,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
// Helper function to render navigation buttons with URL support
|
||||
const renderNavButton = (config: ButtonConfig, index: number) => {
|
||||
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
||||
|
||||
|
||||
// Check if this button has URL navigation support
|
||||
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
||||
? getToolNavigation(config.id)
|
||||
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
||||
? getToolNavigation(config.id)
|
||||
: null;
|
||||
|
||||
const handleClick = (e?: React.MouseEvent) => {
|
||||
@@ -59,7 +62,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
return (
|
||||
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
|
||||
<ActionIcon
|
||||
{...(navProps ? {
|
||||
{...(navProps ? {
|
||||
component: "a" as const,
|
||||
href: navProps.href,
|
||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||
@@ -150,8 +153,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
//},
|
||||
{
|
||||
id: 'config',
|
||||
name: t("quickAccess.config", "Config"),
|
||||
icon: <LocalIcon icon="settings-rounded" width="1.25rem" height="1.25rem" />,
|
||||
name: config?.enableLogin ? t("quickAccess.account", "Account") : t("quickAccess.config", "Config"),
|
||||
icon: config?.enableLogin ? <LocalIcon icon="person-rounded" width="1.25rem" height="1.25rem" /> : <LocalIcon icon="settings-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'lg',
|
||||
type: 'modal',
|
||||
onClick: () => {
|
||||
@@ -217,7 +220,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
<div className="spacer" />
|
||||
|
||||
{/* Config button at the bottom */}
|
||||
{/* {buttonConfigs
|
||||
{buttonConfigs
|
||||
.filter(config => config.id === 'config')
|
||||
.map(config => (
|
||||
<div key={config.id} className="flex flex-col items-center gap-1">
|
||||
@@ -237,16 +240,18 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
{config.name}
|
||||
</span>
|
||||
</div>
|
||||
))} */}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <AppConfigModal
|
||||
<AppConfigModal
|
||||
opened={configModalOpen}
|
||||
onClose={() => setConfigModalOpen(false)}
|
||||
/> */}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
QuickAccessBar.displayName = 'QuickAccessBar';
|
||||
|
||||
export default QuickAccessBar;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { createContext, useContext, ReactNode } from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { useRainbowTheme } from '../../hooks/useRainbowTheme';
|
||||
import { mantineTheme } from '../../theme/mantineTheme';
|
||||
|
||||
@@ -14,6 +14,8 @@ import { Tooltip } from '../shared/Tooltip';
|
||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { SearchInterface } from '../viewer/SearchInterface';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
import ViewerAnnotationControls from './rightRail/ViewerAnnotationControls';
|
||||
|
||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||
|
||||
@@ -43,6 +45,9 @@ export default function RightRail() {
|
||||
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
|
||||
const { removeFiles } = useFileManagement();
|
||||
|
||||
// Signature context for checking if signatures have been applied
|
||||
const { signaturesApplied } = useSignature();
|
||||
|
||||
const activeFiles = selectors.getFiles();
|
||||
const filesSignature = selectors.getFilesSignature();
|
||||
|
||||
@@ -98,8 +103,14 @@ export default function RightRail() {
|
||||
}
|
||||
}, [currentView, setSelectedFiles, pageEditorFunctions]);
|
||||
|
||||
const handleExportAll = useCallback(() => {
|
||||
const handleExportAll = useCallback(async () => {
|
||||
if (currentView === 'viewer') {
|
||||
// Check if signatures have been applied
|
||||
if (!signaturesApplied) {
|
||||
alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use EmbedPDF export functionality for viewer mode
|
||||
viewerContext?.exportActions?.download();
|
||||
} else if (currentView === 'fileEditor') {
|
||||
@@ -119,7 +130,7 @@ export default function RightRail() {
|
||||
// Export all pages (not just selected)
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
}
|
||||
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext]);
|
||||
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext, signaturesApplied, selectors, fileActions]);
|
||||
|
||||
const handleCloseSelected = useCallback(() => {
|
||||
if (currentView !== 'fileEditor') return;
|
||||
@@ -283,6 +294,9 @@ export default function RightRail() {
|
||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Controls */}
|
||||
<ViewerAnnotationControls currentView={currentView} />
|
||||
</div>
|
||||
<Divider className="right-rail-divider" />
|
||||
</div>
|
||||
|
||||
@@ -107,3 +107,5 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TextInput.displayName = 'TextInput';
|
||||
|
||||
@@ -32,7 +32,7 @@ const ToolChain: React.FC<ToolChainProps> = ({
|
||||
|
||||
const getToolName = (toolId: ToolId) => {
|
||||
return t(`home.${toolId}.title`, toolId);
|
||||
}
|
||||
};
|
||||
|
||||
// Create full tool chain for tooltip
|
||||
const fullChainDisplay = displayStyle === 'badges' ? (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
interface ToolIconProps {
|
||||
icon: React.ReactNode;
|
||||
opacity?: number;
|
||||
color?: string;
|
||||
marginRight?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared icon component for consistent tool icon styling across the application.
|
||||
* Uses the same visual pattern as ToolButton: scaled to 0.8, centered transform, consistent spacing.
|
||||
*/
|
||||
export const ToolIcon: React.FC<ToolIconProps> = ({
|
||||
icon,
|
||||
opacity = 1,
|
||||
color = "var(--tools-text-and-icon-color)",
|
||||
marginRight = "0.5rem"
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="tool-button-icon"
|
||||
style={{
|
||||
color,
|
||||
marginRight,
|
||||
transform: "scale(0.8)",
|
||||
transformOrigin: "center",
|
||||
opacity
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,11 +15,11 @@ const viewOptionStyle = {
|
||||
gap: 6,
|
||||
whiteSpace: 'nowrap',
|
||||
paddingTop: '0.3rem',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Build view options showing text always
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => {
|
||||
const viewerOption = {
|
||||
label: (
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
@@ -75,7 +75,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
// Build options array conditionally
|
||||
return [
|
||||
viewerOption,
|
||||
...(isToolSelected ? [] : [pageEditorOption]),
|
||||
pageEditorOption,
|
||||
fileEditorOption,
|
||||
];
|
||||
};
|
||||
@@ -83,19 +83,15 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
interface TopControlsProps {
|
||||
currentView: WorkbenchType;
|
||||
setCurrentView: (view: WorkbenchType) => void;
|
||||
selectedToolKey?: string | null;
|
||||
}
|
||||
|
||||
const TopControls = ({
|
||||
currentView,
|
||||
setCurrentView,
|
||||
selectedToolKey,
|
||||
}: TopControlsProps) => {
|
||||
}: TopControlsProps) => {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||
|
||||
const isToolSelected = selectedToolKey !== null;
|
||||
|
||||
const handleViewChange = useCallback((view: string) => {
|
||||
if (!isValidWorkbench(view)) {
|
||||
return;
|
||||
@@ -122,7 +118,7 @@ const TopControls = ({
|
||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data={createViewOptions(currentView, switchingTo, isToolSelected)}
|
||||
data={createViewOptions(currentView, switchingTo)}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
color="blue"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { NavKey } from './types';
|
||||
import HotkeysSection from './configSections/HotkeysSection';
|
||||
import GeneralSection from './configSections/GeneralSection';
|
||||
|
||||
export interface ConfigNavItem {
|
||||
key: NavKey;
|
||||
label: string;
|
||||
icon: string;
|
||||
component: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ConfigNavSection {
|
||||
title: string;
|
||||
items: ConfigNavItem[];
|
||||
}
|
||||
|
||||
export interface ConfigColors {
|
||||
navBg: string;
|
||||
sectionTitle: string;
|
||||
navItem: string;
|
||||
navItemActive: string;
|
||||
navItemActiveBg: string;
|
||||
contentBg: string;
|
||||
headerBorder: string;
|
||||
}
|
||||
|
||||
export const createConfigNavSections = (
|
||||
Overview: React.ComponentType<{ onLogoutClick: () => void }>,
|
||||
onLogoutClick: () => void
|
||||
): ConfigNavSection[] => {
|
||||
const sections: ConfigNavSection[] = [
|
||||
{
|
||||
title: 'Account',
|
||||
items: [
|
||||
{
|
||||
key: 'overview',
|
||||
label: 'Overview',
|
||||
icon: 'person-rounded',
|
||||
component: <Overview onLogoutClick={onLogoutClick} />
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Preferences',
|
||||
items: [
|
||||
{
|
||||
key: 'general',
|
||||
label: 'General',
|
||||
icon: 'settings-rounded',
|
||||
component: <GeneralSection />
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard Shortcuts',
|
||||
icon: 'keyboard-rounded',
|
||||
component: <HotkeysSection />
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return sections;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Paper, Stack, Switch, Text, Tooltip, NumberInput } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePreferences } from '../../../../contexts/PreferencesContext';
|
||||
|
||||
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
|
||||
|
||||
const GeneralSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { preferences, updatePreference } = usePreferences();
|
||||
const [fileLimitInput, setFileLimitInput] = useState<number | string>(preferences.autoUnzipFileLimit);
|
||||
|
||||
// Sync local state with preference changes
|
||||
useEffect(() => {
|
||||
setFileLimitInput(preferences.autoUnzipFileLimit);
|
||||
}, [preferences.autoUnzipFileLimit]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">{t('settings.general.title', 'General')}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('settings.general.description', 'Configure general application preferences.')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Tooltip
|
||||
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.autoUnzip', 'Auto-unzip API responses')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.autoUnzipDescription', 'Automatically extract files from ZIP responses')}
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.autoUnzip}
|
||||
onChange={(event) => updatePreference('autoUnzip', event.currentTarget.checked)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={t('settings.general.autoUnzipFileLimitTooltip', 'Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.')}
|
||||
multiline
|
||||
w={300}
|
||||
withArrow
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'help' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.autoUnzipFileLimit', 'Auto-unzip file limit')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('settings.general.autoUnzipFileLimitDescription', 'Maximum number of files to extract from ZIP')}
|
||||
</Text>
|
||||
</div>
|
||||
<NumberInput
|
||||
value={fileLimitInput}
|
||||
onChange={setFileLimitInput}
|
||||
onBlur={() => {
|
||||
const numValue = Number(fileLimitInput);
|
||||
const finalValue = (!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100) ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue;
|
||||
setFileLimitInput(finalValue);
|
||||
updatePreference('autoUnzipFileLimit', finalValue);
|
||||
}}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
disabled={!preferences.autoUnzip}
|
||||
style={{ width: 90 }}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSection;
|
||||
@@ -0,0 +1,173 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Badge, Box, Button, Divider, Group, Paper, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
|
||||
import { useHotkeys } from '../../../../contexts/HotkeyContext';
|
||||
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
|
||||
import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
|
||||
import { ToolId } from 'src/types/toolId';
|
||||
import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
|
||||
|
||||
const rowStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
};
|
||||
|
||||
const rowHeaderStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.5rem',
|
||||
};
|
||||
|
||||
const HotkeysSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toolRegistry } = useToolWorkflow();
|
||||
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
|
||||
const [editingTool, setEditingTool] = useState<ToolId | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingTool) {
|
||||
return;
|
||||
}
|
||||
pauseHotkeys();
|
||||
return () => {
|
||||
resumeHotkeys();
|
||||
};
|
||||
}, [editingTool, pauseHotkeys, resumeHotkeys]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingTool) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
setEditingTool(null);
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const binding = eventToBinding(event as KeyboardEvent);
|
||||
if (!binding) {
|
||||
const osKey = isMac ? 'mac' : 'windows';
|
||||
const fallbackText = isMac
|
||||
? 'Include ⌘ (Command), ⌥ (Option), or another modifier in your shortcut.'
|
||||
: 'Include Ctrl, Alt, or another modifier in your shortcut.';
|
||||
setError(t(`settings.hotkeys.errorModifier.${osKey}`, fallbackText));
|
||||
return;
|
||||
}
|
||||
|
||||
const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => (
|
||||
toolId !== editingTool && bindingEquals(existing, binding)
|
||||
));
|
||||
|
||||
if (conflictEntry) {
|
||||
const conflictTool = toolRegistry[conflictEntry[0]]?.name ?? conflictEntry[0];
|
||||
setError(t('settings.hotkeys.errorConflict', 'Shortcut already used by {{tool}}.', { tool: conflictTool }));
|
||||
return;
|
||||
}
|
||||
|
||||
updateHotkey(editingTool, binding);
|
||||
setEditingTool(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, true);
|
||||
};
|
||||
}, [editingTool, hotkeys, toolRegistry, updateHotkey, t]);
|
||||
|
||||
const handleStartCapture = (toolId: ToolId) => {
|
||||
setEditingTool(toolId);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">Keyboard Shortcuts</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Customize keyboard shortcuts for quick tool access. Click "Change shortcut" and press a new key combination. Press Esc to cancel.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
{tools.map(([toolId, tool], index) => {
|
||||
const currentBinding = hotkeys[toolId];
|
||||
const defaultBinding = defaults[toolId];
|
||||
const isEditing = editingTool === toolId;
|
||||
const defaultParts = getDisplayParts(defaultBinding);
|
||||
const defaultLabel = defaultParts.length > 0
|
||||
? defaultParts.join(' + ')
|
||||
: t('settings.hotkeys.none', 'Not assigned');
|
||||
|
||||
return (
|
||||
<React.Fragment key={toolId}>
|
||||
<Box style={rowStyle} data-testid={`hotkey-row-${toolId}`}>
|
||||
<div style={rowHeaderStyle}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', minWidth: 0 }}>
|
||||
<Text fw={600}>{tool.name}</Text>
|
||||
<Group gap="xs" wrap="wrap" align="center">
|
||||
<HotkeyDisplay binding={currentBinding} size="md" />
|
||||
{!bindingEquals(currentBinding, defaultBinding) && (
|
||||
<Badge variant="light" color="orange" radius="sm">
|
||||
{t('settings.hotkeys.customBadge', 'Custom')}
|
||||
</Badge>
|
||||
)}
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('settings.hotkeys.defaultLabel', 'Default: {{shortcut}}', { shortcut: defaultLabel })}
|
||||
</Text>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
variant={isEditing ? 'filled' : 'default'}
|
||||
color={isEditing ? 'blue' : undefined}
|
||||
onClick={() => handleStartCapture(toolId)}
|
||||
>
|
||||
{isEditing
|
||||
? t('settings.hotkeys.capturing', 'Press keys… (Esc to cancel)')
|
||||
: t('settings.hotkeys.change', 'Change shortcut')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
disabled={bindingEquals(currentBinding, defaultBinding)}
|
||||
onClick={() => resetHotkey(toolId)}
|
||||
>
|
||||
{t('settings.hotkeys.reset', 'Reset')}
|
||||
</Button>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
{isEditing && error && (
|
||||
<Alert color="red" radius="sm" variant="filled">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{index < tools.length - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HotkeysSection;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, Code, Group, Badge, Alert, Loader } from '@mantine/core';
|
||||
import { useAppConfig } from '../../../../hooks/useAppConfig';
|
||||
|
||||
const Overview: React.FC = () => {
|
||||
const { config, loading, error } = useAppConfig();
|
||||
|
||||
const renderConfigSection = (title: string, data: any) => {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
|
||||
return (
|
||||
<Stack gap="xs" mb="md">
|
||||
<Text fw={600} size="md" c="blue">{title}</Text>
|
||||
<Stack gap="xs" pl="md">
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<Group key={key} wrap="nowrap" align="flex-start">
|
||||
<Text size="sm" w={150} style={{ flexShrink: 0 }} c="dimmed">
|
||||
{key}:
|
||||
</Text>
|
||||
{typeof value === 'boolean' ? (
|
||||
<Badge color={value ? 'green' : 'red'} size="sm">
|
||||
{value ? 'true' : 'false'}
|
||||
</Badge>
|
||||
) : typeof value === 'object' ? (
|
||||
<Code block>{JSON.stringify(value, null, 2)}</Code>
|
||||
) : (
|
||||
String(value) || 'null'
|
||||
)}
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const basicConfig = config ? {
|
||||
appName: config.appName,
|
||||
appNameNavbar: config.appNameNavbar,
|
||||
baseUrl: config.baseUrl,
|
||||
contextPath: config.contextPath,
|
||||
serverPort: config.serverPort,
|
||||
} : null;
|
||||
|
||||
const securityConfig = config ? {
|
||||
enableLogin: config.enableLogin,
|
||||
} : null;
|
||||
|
||||
const systemConfig = config ? {
|
||||
enableAlphaFunctionality: config.enableAlphaFunctionality,
|
||||
enableAnalytics: config.enableAnalytics,
|
||||
} : null;
|
||||
|
||||
const integrationConfig = config ? {
|
||||
SSOAutoLogin: config.SSOAutoLogin,
|
||||
} : null;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" py="md">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" c="dimmed">Loading configuration...</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert color="red" title="Error">
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Text fw={600} size="lg">Application Configuration</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Current application settings and configuration details.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{config && (
|
||||
<>
|
||||
{renderConfigSection('Basic Configuration', basicConfig)}
|
||||
{renderConfigSection('Security Configuration', securityConfig)}
|
||||
{renderConfigSection('System Configuration', systemConfig)}
|
||||
{renderConfigSection('Integration Configuration', integrationConfig)}
|
||||
|
||||
{config.error && (
|
||||
<Alert color="yellow" title="Configuration Warning">
|
||||
{config.error}
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
@@ -0,0 +1,19 @@
|
||||
export type NavKey =
|
||||
| 'overview'
|
||||
| 'preferences'
|
||||
| 'notifications'
|
||||
| 'connections'
|
||||
| 'general'
|
||||
| 'people'
|
||||
| 'teams'
|
||||
| 'security'
|
||||
| 'identity'
|
||||
| 'plan'
|
||||
| 'payments'
|
||||
| 'requests'
|
||||
| 'developer'
|
||||
| 'api-keys'
|
||||
| 'hotkeys';
|
||||
|
||||
|
||||
// some of these are not used yet, but appear in figma designs
|
||||
@@ -19,7 +19,7 @@ const DocumentThumbnail: React.FC<DocumentThumbnailProps> = ({
|
||||
children
|
||||
}) => {
|
||||
if (!file) return null;
|
||||
|
||||
|
||||
const containerStyle = {
|
||||
position: 'relative' as const,
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
@@ -36,6 +36,7 @@ const DocumentThumbnail: React.FC<DocumentThumbnailProps> = ({
|
||||
return (
|
||||
<Box style={containerStyle} onClick={onClick}>
|
||||
<Image
|
||||
className='ph-no-capture'
|
||||
src={thumbnail}
|
||||
alt={`Preview of ${file.name}`}
|
||||
fit="contain"
|
||||
@@ -49,11 +50,12 @@ const DocumentThumbnail: React.FC<DocumentThumbnailProps> = ({
|
||||
return (
|
||||
<Box style={containerStyle} onClick={onClick}>
|
||||
<Center style={{ width: '100%', height: '100%', backgroundColor: 'var(--mantine-color-gray-1)', borderRadius: '0.25rem' }}>
|
||||
<PictureAsPdfIcon
|
||||
style={{
|
||||
fontSize: '2rem',
|
||||
color: 'var(--mantine-color-gray-6)'
|
||||
}}
|
||||
<PictureAsPdfIcon
|
||||
className='ph-no-capture'
|
||||
style={{
|
||||
fontSize: '2rem',
|
||||
color: 'var(--mantine-color-gray-6)'
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
{children}
|
||||
@@ -61,4 +63,4 @@ const DocumentThumbnail: React.FC<DocumentThumbnailProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentThumbnail;
|
||||
export default DocumentThumbnail;
|
||||
|
||||
@@ -33,8 +33,11 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
const { getHomeNavigation } = useSidebarNavigation();
|
||||
|
||||
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
||||
// Special case: multiTool should always show even when sidebars are hidden
|
||||
const indicatorShouldShow = Boolean(
|
||||
selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)
|
||||
selectedToolKey &&
|
||||
((leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)) ||
|
||||
selectedToolKey === 'multiTool')
|
||||
);
|
||||
|
||||
// Local animation and hover state
|
||||
@@ -47,7 +50,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
const animTimeoutRef = useRef<number | null>(null);
|
||||
const replayRafRef = useRef<number | null>(null);
|
||||
|
||||
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey };
|
||||
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey; };
|
||||
|
||||
const clearTimers = () => {
|
||||
if (collapseTimeoutRef.current) {
|
||||
@@ -78,7 +81,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
setReplayAnim(false);
|
||||
animTimeoutRef.current = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const firstShow = () => {
|
||||
clearTimers();
|
||||
@@ -88,7 +91,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
animTimeoutRef.current = window.setTimeout(() => {
|
||||
animTimeoutRef.current = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerCollapse = () => {
|
||||
clearTimers();
|
||||
@@ -98,7 +101,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
prevKeyRef.current = null;
|
||||
collapseTimeoutRef.current = null;
|
||||
}, 500); // match CSS transition duration
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (indicatorShouldShow) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const isNavButtonActive = (
|
||||
isFilesModalOpen: boolean,
|
||||
configModalOpen: boolean,
|
||||
selectedToolKey?: string | null,
|
||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
||||
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||
): boolean => {
|
||||
const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id;
|
||||
const isActiveByContext =
|
||||
@@ -35,7 +35,7 @@ export const getNavButtonStyle = (
|
||||
isFilesModalOpen: boolean,
|
||||
configModalOpen: boolean,
|
||||
selectedToolKey?: string | null,
|
||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
||||
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||
) => {
|
||||
const isActive = isNavButtonActive(
|
||||
config,
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.right-rail-slot.visible {
|
||||
max-height: 18rem; /* increased to fit additional controls + divider */
|
||||
max-height: 40rem; /* increased to fit additional controls + divider */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -77,14 +77,14 @@
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
max-height: 18rem;
|
||||
max-height: 40rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rightRailShrinkUp {
|
||||
0% {
|
||||
max-height: 18rem;
|
||||
max-height: 40rem;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionIcon, Popover } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LocalIcon from '../LocalIcon';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { ViewerContext } from '../../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../../contexts/SignatureContext';
|
||||
import { ColorSwatchButton, ColorPicker } from '../../annotation/shared/ColorPicker';
|
||||
import { useFileState, useFileContext } from '../../../contexts/FileContext';
|
||||
import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
||||
import { createProcessedFile } from '../../../contexts/file/fileActions';
|
||||
import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
|
||||
|
||||
interface ViewerAnnotationControlsProps {
|
||||
currentView: string;
|
||||
}
|
||||
|
||||
export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
|
||||
|
||||
// Viewer context for PDF controls - safely handle when not available
|
||||
const viewerContext = React.useContext(ViewerContext);
|
||||
|
||||
// Signature context for accessing drawing API
|
||||
const { signatureApiRef } = useSignature();
|
||||
|
||||
// File state for save functionality
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions: fileActions } = useFileContext();
|
||||
const activeFiles = selectors.getFiles();
|
||||
|
||||
// Turn off annotation mode when switching away from viewer
|
||||
useEffect(() => {
|
||||
if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
|
||||
viewerContext.setAnnotationMode(false);
|
||||
}
|
||||
}, [currentView, viewerContext]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Annotation Visibility Toggle */}
|
||||
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationsVisibility();
|
||||
}}
|
||||
disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode}
|
||||
>
|
||||
<LocalIcon
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Mode Toggle with Drawing Controls */}
|
||||
{viewerContext?.isAnnotationMode ? (
|
||||
// When active: Show color picker on hover
|
||||
<div
|
||||
onMouseEnter={() => setIsHoverColorPickerOpen(true)}
|
||||
onMouseLeave={() => setIsHoverColorPickerOpen(false)}
|
||||
style={{ display: 'inline-flex' }}
|
||||
>
|
||||
<Popover
|
||||
opened={isHoverColorPickerOpen}
|
||||
onClose={() => setIsHoverColorPickerOpen(false)}
|
||||
position="left"
|
||||
withArrow
|
||||
shadow="md"
|
||||
offset={8}
|
||||
>
|
||||
<Popover.Target>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color="blue"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationMode();
|
||||
setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off
|
||||
// Deactivate drawing tool when exiting annotation mode
|
||||
if (signatureApiRef?.current) {
|
||||
try {
|
||||
signatureApiRef.current.deactivateTools();
|
||||
} catch (error) {
|
||||
console.log('Signature API not ready:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
aria-label="Drawing mode active"
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<div style={{ minWidth: '8rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem', padding: '0.5rem' }}>
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 500 }}>Drawing Color</div>
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
size={32}
|
||||
onClick={() => {
|
||||
setIsHoverColorPickerOpen(false); // Close hover picker
|
||||
setIsColorPickerOpen(true); // Open main color picker modal
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</div>
|
||||
) : (
|
||||
// When inactive: Show "Draw" tooltip
|
||||
<Tooltip content={t('rightRail.draw', 'Draw')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => {
|
||||
viewerContext?.toggleAnnotationMode();
|
||||
// Activate ink drawing tool when entering annotation mode
|
||||
if (signatureApiRef?.current && currentView === 'viewer') {
|
||||
try {
|
||||
signatureApiRef.current.activateDrawMode();
|
||||
signatureApiRef.current.updateDrawSettings(selectedColor, 2);
|
||||
} catch (error) {
|
||||
console.log('Signature API not ready:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Save PDF with Annotations */}
|
||||
<Tooltip content={t('rightRail.save', 'Save')} position="left" offset={12} arrow>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={async () => {
|
||||
if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') {
|
||||
try {
|
||||
const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy();
|
||||
if (pdfArrayBuffer) {
|
||||
// Create new File object with flattened annotations
|
||||
const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Get the original file name or use a default
|
||||
const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf';
|
||||
const newFile = new File([blob], originalFileName, { type: 'application/pdf' });
|
||||
|
||||
// Replace the current file in context with the saved version (exact same logic as Sign tool)
|
||||
if (activeFiles.length > 0) {
|
||||
// Generate thumbnail and metadata for the saved file
|
||||
const thumbnailResult = await generateThumbnailWithMetadata(newFile);
|
||||
const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail);
|
||||
|
||||
// Get current file info
|
||||
const currentFileIds = state.files.ids;
|
||||
if (currentFileIds.length > 0) {
|
||||
const currentFileId = currentFileIds[0];
|
||||
const currentRecord = selectors.getStirlingFileStub(currentFileId);
|
||||
|
||||
if (!currentRecord) {
|
||||
console.error('No file record found for:', currentFileId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create output stub and file (exact same as Sign tool)
|
||||
const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata);
|
||||
const outputStirlingFile = createStirlingFile(newFile, outputStub.id);
|
||||
|
||||
// Replace the original file with the saved version
|
||||
await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving PDF:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer'}
|
||||
>
|
||||
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={(color) => {
|
||||
setSelectedColor(color);
|
||||
// Update drawing tool color if annotation mode is active
|
||||
if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') {
|
||||
try {
|
||||
signatureApiRef.current.updateDrawSettings(color, 2);
|
||||
} catch (error) {
|
||||
console.log('Unable to update drawing settings:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
title="Choose Drawing Color"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Slider, Text, Group, NumberInput } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export default function SliderWithInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
min = 0,
|
||||
max = 200,
|
||||
step = 1,
|
||||
}: Props) {
|
||||
return (
|
||||
<div>
|
||||
<Text size="sm" fw={600} mb={4}>{label}: {Math.round(value)}%</Text>
|
||||
<Group gap="sm" align="center">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Slider min={min} max={max} step={step} value={value} onChange={onChange} disabled={disabled} />
|
||||
</div>
|
||||
<NumberInput
|
||||
value={value}
|
||||
onChange={(v) => onChange(Number(v) || 0)}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
disabled={disabled}
|
||||
style={{ width: 90 }}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { useToast } from './ToastContext';
|
||||
import { ToastInstance, ToastLocation } from './types';
|
||||
import { LocalIcon } from '../shared/LocalIcon';
|
||||
@@ -66,7 +65,7 @@ export default function ToastRenderer() {
|
||||
<LocalIcon icon={`material-symbols:${getDefaultIconName(t)}`} width={20} height={20} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Title + count badge */}
|
||||
<div className="toast-title-container">
|
||||
<span>{t.title}</span>
|
||||
@@ -74,7 +73,7 @@ export default function ToastRenderer() {
|
||||
<span className="toast-count-badge">{t.count}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Controls */}
|
||||
<div className="toast-controls">
|
||||
{t.expandable && (
|
||||
@@ -101,20 +100,20 @@ export default function ToastRenderer() {
|
||||
{/* Progress bar - always show when present */}
|
||||
{typeof t.progress === 'number' && (
|
||||
<div className="toast-progress-container">
|
||||
<div
|
||||
<div
|
||||
className={getProgressBarClass(t)}
|
||||
style={{ width: `${t.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Body content - only show when expanded */}
|
||||
{(t.isExpanded || !t.expandable) && (
|
||||
<div className="toast-body">
|
||||
{t.body}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Button - always show when present, positioned below body */}
|
||||
{t.buttonText && t.buttonCallback && (
|
||||
<div className="toast-action-container">
|
||||
|
||||
@@ -7,9 +7,10 @@ import { useToolSections } from '../../hooks/useToolSections';
|
||||
import SubcategoryHeader from './shared/SubcategoryHeader';
|
||||
import NoToolsFound from './shared/NoToolsFound';
|
||||
import "./toolPicker/ToolPicker.css";
|
||||
import { ToolId } from 'src/types/toolId';
|
||||
|
||||
interface SearchResultsProps {
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||
onSelect: (id: string) => void;
|
||||
searchQuery?: string;
|
||||
}
|
||||
@@ -40,13 +41,13 @@ const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect,
|
||||
{group.tools.map(({ id, tool }) => {
|
||||
const matchedText = matchedTextMap.get(id);
|
||||
// Check if the match was from synonyms and show the actual synonym that matched
|
||||
const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
|
||||
const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
|
||||
matchedText.toLowerCase().includes(synonym.toLowerCase())
|
||||
);
|
||||
const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
|
||||
const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
|
||||
matchedText.toLowerCase().includes(synonym.toLowerCase())
|
||||
) : undefined;
|
||||
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
|
||||
@@ -10,5 +10,5 @@ export default function ToolLoadingFallback({ toolName }: { toolName?: string })
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import ToolSearch from './toolPicker/ToolSearch';
|
||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import { ScrollArea } from '@mantine/core';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
// No props needed - component uses context
|
||||
|
||||
@@ -14,6 +16,7 @@ export default function ToolPanel() {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef } = sidebarRefs;
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate prop drilling
|
||||
@@ -33,17 +36,17 @@ export default function ToolPanel() {
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||
}`}
|
||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'}`}
|
||||
style={{
|
||||
width: isPanelVisible ? '18.5rem' : '0',
|
||||
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
|
||||
padding: '0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: isPanelVisible ? 1 : 0,
|
||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
@@ -71,7 +74,7 @@ export default function ToolPanel() {
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SearchResults
|
||||
filteredTools={filteredTools}
|
||||
onSelect={handleToolSelect}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
@@ -80,7 +83,7 @@ export default function ToolPanel() {
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
|
||||
@@ -6,11 +6,12 @@ import "./toolPicker/ToolPicker.css";
|
||||
import { useToolSections } from "../../hooks/useToolSections";
|
||||
import NoToolsFound from "./shared/NoToolsFound";
|
||||
import { renderToolButtons } from "./shared/renderToolButtons";
|
||||
import { ToolId } from "src/types/toolId";
|
||||
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||
isSearching?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
||||
import { BaseToolProps } from "../../types/tool";
|
||||
import ToolLoadingFallback from "./ToolLoadingFallback";
|
||||
import { ToolId } from "src/types/toolId";
|
||||
|
||||
interface ToolRendererProps extends BaseToolProps {
|
||||
selectedToolKey: string;
|
||||
selectedToolKey: ToolId;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +27,7 @@ const ToolRenderer = ({
|
||||
|
||||
// Wrap lazy-loaded component with Suspense
|
||||
return (
|
||||
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
|
||||
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
|
||||
<ToolComponent
|
||||
onPreviewFile={onPreviewFile}
|
||||
onComplete={onComplete}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* AddAttachmentsSettings - Shared settings component for both tool UI and automation
|
||||
*
|
||||
* Allows selecting files to attach to PDFs.
|
||||
*/
|
||||
|
||||
import { Stack, Text, Group, ActionIcon, Alert, ScrollArea, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddAttachmentsParameters } from "../../../hooks/tools/addAttachments/useAddAttachmentsParameters";
|
||||
import LocalIcon from "../../shared/LocalIcon";
|
||||
|
||||
interface AddAttachmentsSettingsProps {
|
||||
parameters: AddAttachmentsParameters;
|
||||
onParameterChange: <K extends keyof AddAttachmentsParameters>(key: K, value: AddAttachmentsParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AddAttachmentsSettings = ({ parameters, onParameterChange, disabled = false }: AddAttachmentsSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Alert color="blue" variant="light">
|
||||
<Text size="sm">
|
||||
{t("AddAttachmentsRequest.info", "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.")}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("AddAttachmentsRequest.selectFiles", "Select Files to Attach")}
|
||||
</Text>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
// Append to existing attachments instead of replacing
|
||||
const newAttachments = [...(parameters.attachments || []), ...files];
|
||||
onParameterChange('attachments', newAttachments);
|
||||
// Reset the input so the same file can be selected again
|
||||
e.target.value = '';
|
||||
}}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
id="attachments-input"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
component="label"
|
||||
htmlFor="attachments-input"
|
||||
disabled={disabled}
|
||||
leftSection={<LocalIcon icon="plus" width="14" height="14" />}
|
||||
>
|
||||
{parameters.attachments?.length > 0
|
||||
? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
|
||||
: t("AddAttachmentsRequest.placeholder", "Choose files...")
|
||||
}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{parameters.attachments?.length > 0 && (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("AddAttachmentsRequest.selectedFiles", "Selected Files")} ({parameters.attachments.length})
|
||||
</Text>
|
||||
<ScrollArea.Autosize mah={300} type="scroll" offsetScrollbars styles={{ viewport: { overflowX: 'hidden' } }}>
|
||||
<Stack gap="xs">
|
||||
{parameters.attachments.map((file, index) => (
|
||||
<Group key={index} justify="space-between" p="xs" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 'var(--mantine-radius-sm)', alignItems: 'flex-start' }}>
|
||||
<Group gap="xs" style={{ flex: 1, minWidth: 0, alignItems: 'flex-start' }}>
|
||||
{/* Filename (two-line clamp, wraps, no icon on the left) */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 'var(--mantine-font-size-sm)',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.2,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2 as any,
|
||||
WebkitBoxOrient: 'vertical' as any,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</div>
|
||||
</div>
|
||||
<Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
|
||||
({(file.size / 1024).toFixed(1)} KB)
|
||||
</Text>
|
||||
</Group>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
style={{ flexShrink: 0 }}
|
||||
onClick={() => {
|
||||
const newAttachments = (parameters.attachments || []).filter((_, i) => i !== index);
|
||||
onParameterChange('attachments', newAttachments);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="14" height="14" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAttachmentsSettings;
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* AddPageNumbersAppearanceSettings - Customize Appearance step
|
||||
*/
|
||||
|
||||
import { Stack, Select, TextInput, NumberInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
|
||||
import { Tooltip } from "../../shared/Tooltip";
|
||||
|
||||
interface AddPageNumbersAppearanceSettingsProps {
|
||||
parameters: AddPageNumbersParameters;
|
||||
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AddPageNumbersAppearanceSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: AddPageNumbersAppearanceSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Tooltip content={t('marginTooltip', 'Distance between the page number and the edge of the page.')}>
|
||||
<Select
|
||||
label={t('addPageNumbers.selectText.2', 'Margin')}
|
||||
value={parameters.customMargin}
|
||||
onChange={(v) => onParameterChange('customMargin', (v as any) || 'medium')}
|
||||
data={[
|
||||
{ value: 'small', label: t('sizes.small', 'Small') },
|
||||
{ value: 'medium', label: t('sizes.medium', 'Medium') },
|
||||
{ value: 'large', label: t('sizes.large', 'Large') },
|
||||
{ value: 'x-large', label: t('sizes.x-large', 'Extra Large') },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t('fontSizeTooltip', 'Size of the page number text in points. Larger numbers create bigger text.')}>
|
||||
<NumberInput
|
||||
label={t('addPageNumbers.fontSize', 'Font Size')}
|
||||
value={parameters.fontSize}
|
||||
onChange={(v) => onParameterChange('fontSize', typeof v === 'number' ? v : 12)}
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t('fontTypeTooltip', 'Font family for the page numbers. Choose based on your document style.')}>
|
||||
<Select
|
||||
label={t('addPageNumbers.fontName', 'Font Type')}
|
||||
value={parameters.fontType}
|
||||
onChange={(v) => onParameterChange('fontType', (v as any) || 'Times')}
|
||||
data={[
|
||||
{ value: 'Times', label: 'Times Roman' },
|
||||
{ value: 'Helvetica', label: 'Helvetica' },
|
||||
{ value: 'Courier', label: 'Courier New' },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t('customTextTooltip', 'Optional custom format for page numbers. Use {n} as placeholder for the number. Example: "Page {n}" will show "Page 1", "Page 2", etc.')}>
|
||||
<TextInput
|
||||
label={t('addPageNumbers.selectText.6', 'Custom Text Format')}
|
||||
value={parameters.customText || ''}
|
||||
onChange={(e) => onParameterChange('customText', e.currentTarget.value)}
|
||||
placeholder={t('addPageNumbers.customNumberDesc', 'e.g., "Page {n}" or leave blank for just numbers')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPageNumbersAppearanceSettings;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* AddPageNumbersAutomationSettings - Used for automation only
|
||||
*
|
||||
* Combines both position and appearance settings into a single view
|
||||
*/
|
||||
|
||||
import { Stack, Divider, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
|
||||
import AddPageNumbersPositionSettings from "./AddPageNumbersPositionSettings";
|
||||
import AddPageNumbersAppearanceSettings from "./AddPageNumbersAppearanceSettings";
|
||||
|
||||
interface AddPageNumbersAutomationSettingsProps {
|
||||
parameters: AddPageNumbersParameters;
|
||||
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AddPageNumbersAutomationSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false
|
||||
}: AddPageNumbersAutomationSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Position & Pages Section */}
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={600}>{t("addPageNumbers.positionAndPages", "Position & Pages")}</Text>
|
||||
<AddPageNumbersPositionSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
file={null}
|
||||
showQuickGrid={true}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Appearance Section */}
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={600}>{t("addPageNumbers.customize", "Customize Appearance")}</Text>
|
||||
<AddPageNumbersAppearanceSettings
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPageNumbersAutomationSettings;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* AddPageNumbersPositionSettings - Position & Pages step
|
||||
*/
|
||||
|
||||
import { Stack, TextInput, NumberInput, Divider, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddPageNumbersParameters } from "./useAddPageNumbersParameters";
|
||||
import { Tooltip } from "../../shared/Tooltip";
|
||||
import PageNumberPreview from "./PageNumberPreview";
|
||||
|
||||
interface AddPageNumbersPositionSettingsProps {
|
||||
parameters: AddPageNumbersParameters;
|
||||
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
file?: File | null;
|
||||
showQuickGrid?: boolean;
|
||||
}
|
||||
|
||||
const AddPageNumbersPositionSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false,
|
||||
file = null,
|
||||
showQuickGrid = true
|
||||
}: AddPageNumbersPositionSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Position Selection */}
|
||||
<Stack gap="md">
|
||||
<PageNumberPreview
|
||||
parameters={parameters}
|
||||
onParameterChange={onParameterChange}
|
||||
file={file}
|
||||
showQuickGrid={showQuickGrid}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Pages & Starting Number Section */}
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={500} mb="xs">{t('addPageNumbers.pagesAndStarting', 'Pages & Starting Number')}</Text>
|
||||
|
||||
<Tooltip content={t('pageSelectionPrompt', 'Specify which pages to add numbers to. Examples: "1,3,5" for specific pages, "1-5" for ranges, "2n" for even pages, or leave blank for all pages.')}>
|
||||
<TextInput
|
||||
label={t('addPageNumbers.selectText.5', 'Pages to Number')}
|
||||
value={parameters.pagesToNumber || ''}
|
||||
onChange={(e) => onParameterChange('pagesToNumber', e.currentTarget.value)}
|
||||
placeholder={t('addPageNumbers.numberPagesDesc', 'e.g., 1,3,5-8 or leave blank for all pages')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t('startingNumberTooltip', 'The first number to display. Subsequent pages will increment from this number.')}>
|
||||
<NumberInput
|
||||
label={t('addPageNumbers.selectText.4', 'Starting Number')}
|
||||
value={parameters.startingNumber}
|
||||
onChange={(v) => onParameterChange('startingNumber', typeof v === 'number' ? v : 1)}
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPageNumbersPositionSettings;
|
||||
@@ -0,0 +1,101 @@
|
||||
/* PageNumberPreview.module.css - EXACT copy from StampPreview */
|
||||
|
||||
/* Container styles */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.containerWithThumbnail {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.containerWithoutThumbnail {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.containerBorder {
|
||||
border: 1px solid var(--border-default, #333);
|
||||
}
|
||||
|
||||
/* Page thumbnail styles */
|
||||
.pageThumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: grayscale(10%) contrast(95%) brightness(105%);
|
||||
}
|
||||
|
||||
/* Quick grid overlay styles - EXACT copy from stamp */
|
||||
.quickGrid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.gridTile {
|
||||
border: 1px dashed rgba(0, 0, 0, 0.15);
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Position numbers at edges within each tile with extra top/bottom spacing */
|
||||
.gridTile:nth-child(1) { align-items: flex-start; justify-content: flex-start; padding-top: 4px; } /* top-left */
|
||||
.gridTile:nth-child(2) { align-items: flex-start; justify-content: center; padding-top: 4px; } /* top-center */
|
||||
.gridTile:nth-child(3) { align-items: flex-start; justify-content: flex-end; padding-top: 4px; } /* top-right */
|
||||
.gridTile:nth-child(4) { align-items: center; justify-content: flex-start; } /* middle-left */
|
||||
.gridTile:nth-child(5) { align-items: center; justify-content: center; } /* center */
|
||||
.gridTile:nth-child(6) { align-items: center; justify-content: flex-end; } /* middle-right */
|
||||
.gridTile:nth-child(7) { align-items: flex-end; justify-content: flex-start; padding-bottom: 4px; } /* bottom-left */
|
||||
.gridTile:nth-child(8) { align-items: flex-end; justify-content: center; padding-bottom: 4px; } /* bottom-center */
|
||||
.gridTile:nth-child(9) { align-items: flex-end; justify-content: flex-end; padding-bottom: 4px; } /* bottom-right */
|
||||
|
||||
/* Base padding for all tiles */
|
||||
.gridTile {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.gridTileSelected,
|
||||
.gridTileHovered {
|
||||
border: 2px solid var(--mantine-primary-color-filled, #3b82f6);
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Preview header */
|
||||
.previewHeader {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--border-default, #333);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.previewLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Preview disclaimer */
|
||||
.previewDisclaimer {
|
||||
margin-top: 8px;
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user