Compare commits

...
Author SHA1 Message Date
Anthony Stirling 1a8e47898e init france 2025-09-19 12:43:31 +01:00
Anthony Stirling 34316ec6c9 revert US 2025-09-19 12:42:38 +01:00
Anthony Stirling d7e590f24e update some langs 2025-09-19 08:54:48 +01:00
Anthony StirlingandGitHub 49a10a3865 Change fallback base URL to 'https://stirling.com' 2025-09-17 08:34:55 +01:00
190178a471 Feature/v2/filehistory (#4370)
File History

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-09-16 15:08:11 +01:00
8e8b417f5e V2 Tool - Auto split (#4446)
integrated auto split, with flattened split tool

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-09-16 13:08:54 +01:00
a57373b968 V2 Flatten split options to remove layers of drop downs (#4439)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-09-15 16:11:29 +00:00
James BruntonandGitHub 7dad484aa7 Improve type info on param hooks (#4438)
# Description of Changes
Changes it so that callers of `useBaseTool` know what actual type the
parameters hook that they passed in returned, so they can actually make
use of any extra methods that that params hook has.
2025-09-15 14:28:18 +01:00
James BruntonandGitHub cfdb6eaa1e Add Adjust Page Scale tool to V2 (#4429)
# Description of Changes
Add Adjust Page Scale tool to V2
2025-09-12 17:25:22 +01:00
James BruntonandGitHub 8a367aab54 Change tips icon to i circle (#4430)
# Description of Changes

## Before

<img width="102" height="35" alt="image"
src="https://github.com/user-attachments/assets/fcb85906-85b6-41e1-9162-4084c0e684ec"
/>

## After

<img width="103" height="45" alt="image"
src="https://github.com/user-attachments/assets/241d61d8-d3c4-4dbf-a6af-4fda0867734d"
/>
2025-09-10 18:19:05 +01:00
James BruntonandGitHub f3fd85d777 Add Merge UI to V2 (#4235)
# Description of Changes
Add UI for Merge into V2.
2025-09-10 13:06:23 +00:00
James BruntonandGitHub 9d723eae69 Add auto-redact to V2 (#4417)
# Description of Changes
Adds auto-redact tool to V2, with manual-redact in the UI but explicitly
disabled.

Also creates a shared component for the large buttons we're using in a
couple different tools and uses consistently.
2025-09-10 14:03:11 +01:00
James BruntonandGitHub 494ef801a2 Improve npm scripts (#4424)
# Description of Changes
Change NPM scripts so they call each other (single source of truth) and
add a command to run type checking, linting and tests (to give
confidence CI will pass).
2025-09-09 16:18:09 +01:00
e8af4f6b35 Set i18n to load only current language (#4359)
This pull request introduces a minor configuration change to the i18n
setup in the frontend. The change improves language loading behavior by
ensuring only the current language is loaded, which can help optimize
performance and prevent unnecessary resource usage.

* Added the `load: 'currentOnly'` option to the i18n initialization in
`frontend/src/i18n.ts`, so only the current language is loaded.

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-09-08 10:05:49 +01:00
stirlingbot[bot]GitHubstirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>ConnorYoh
c25985e49e Update Frontend 3rd Party Licenses (#4319)
Auto-generated by stirlingbot[bot]

This PR updates the frontend license report based on changes to
package.json dependencies.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-09-08 08:58:22 +00:00
James BruntonandGitHub 316be5eac5 Fix types of onParameterChange methods (#4415)
# Description of Changes
Fix types of onParameterChange methods
2025-09-08 09:55:30 +01:00
11d23a2d43 V2 Auto rename (#4244)
# Description of Changes

This pull request introduces the new "Auto Rename PDF" tool to the
frontend, enabling users to automatically rename PDF files based on
their content. The implementation includes UI components, parameter
handling, operation logic, localization, and enhancements to the file
response utilities to support backend-provided filenames. Below are the
most important changes grouped by theme:

**Feature: Auto Rename PDF Tool**

- Added the main `AutoRename` tool component (`AutoRename.tsx`) and
registered it in the tool registry, enabling selection and execution of
the auto-rename operation in the UI.
[[1]](diffhunk://#diff-3647ca39d46d109d122d4cd6cbfe981beb4189d05b1b446e5c46824eb98a4a88R1-R80)
[[2]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969R17)
[[3]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969L359-R366)
[[4]](diffhunk://#diff-29427b8d06a23772c56645fc4b72af2980c813605abc162e3d47c2e39d026d06L25-R26)
- Implemented the settings panel (`AutoRenameSettings.tsx`) and
parameter management hook (`useAutoRenameParameters.ts`), allowing users
to configure options such as using the first text as a fallback for the
filename.
[[1]](diffhunk://#diff-b2f9474c8e5a7a42df00a12ffd2d31a785895fe1096e8ca515e6af5633a4d648R1-R27)
[[2]](diffhunk://#diff-8798a1ef451233bf3a1bf8825c12c5b434ad1a17a1beb1ca21fd972fdaceb50cR1-R19)
- Created the operation hook (`useAutoRenameOperation.ts`) to handle API
requests, error handling, and result processing for the auto-rename
feature.

**Localization**

- Added English (US and GB) translations for the new tool, including UI
labels, descriptions, error messages, and settings.
[[1]](diffhunk://#diff-e4d543afa388d9eb8a423e45dfebb91641e3558d00848d70b285ebb91c40b249R1048-R1066)
[[2]](diffhunk://#diff-14c707e28788a3a84ed5293ff6689be73d4bca00e155beaf090f9b37c978babbR1321-R1339)

**File Response Handling Enhancements**

- Updated the file response processor and related hooks to support
preserving backend-provided filenames via the `Content-Disposition`
header, ensuring files are renamed according to backend results.
[[1]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0R11)
[[2]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0L49-R51)
[[3]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191R52-R58)
[[4]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191L175-R183)
[[5]](diffhunk://#diff-fa8af80f4d87370d58e3a5b79df675d201f0c3aa753eda89cec03ff027c4213dL13-R21)
[[6]](diffhunk://#diff-efa525dbdeceaeb5701aa3d2303bf1d533541f65a92d985f94f33b8e87b036d1R2-R37)

These changes collectively deliver a new advanced tool for users to
automatically rename PDFs, with robust parameter handling, user
interface integration, and proper handling of filenames as determined by
backend logic.
---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
2025-09-05 17:12:52 +01:00
1898df0df9 Urls for tools for scrapers and open in new tab support (#4364)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-09-05 11:35:17 +00:00
da359d329d V2 flatten (#4358)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-09-05 11:25:30 +00:00
James BruntonandGitHub bd13f6bf57 Enable ESLint no-unused-vars rule (#4367)
# Description of Changes
Enable ESLint [no-unused-vars
rule](https://typescript-eslint.io/rules/no-unused-vars/)
2025-09-05 11:16:17 +00:00
87c63efcec Feature/v2/filewithid implementation (#4369)
Added Filewithid type
Updated code where file was being used to use filewithid
Updated places we identified files by name or composite keys to use UUID
Updated places we should have been using quickkey
Updated pageeditor issue where we parsed pagenumber from pageid instead
of using pagenumber directly

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-09-05 11:33:03 +01:00
James BruntonandGitHub 5caec41d96 Enable ESLint no-empty-object-type rule (#4354)
# Description of Changes
Enable ESLint [no-empty-object-type
rule](https://typescript-eslint.io/rules/no-empty-object-type/)
2025-09-05 10:15:36 +00:00
James BruntonandGitHub d558bb5fac Enable ESLint ban-ts-comment rule (#4350)
# Description of Changes
Enable ESLint [ban-ts-comment
rule](https://typescript-eslint.io/rules/ban-ts-comment/)
2025-09-05 10:02:00 +00:00
James BruntonandGitHub cd1fc682ab Enable ESLint no-case-declarations rule (#4348)
# Description of Changes
Enable ESLint [no-case-declarations
rule](https://eslint.org/docs/latest/rules/no-case-declarations)
2025-09-05 09:58:14 +00:00
James BruntonandGitHub b9cf7e7820 Enable ESLint no-empty-pattern rule (#4343)
# Description of Changes
Enable ESLint [no-empty-pattern
rule](https://eslint.org/docs/latest/rules/no-empty-pattern)
2025-09-05 10:55:03 +01:00
James BruntonandGitHub 94e8f603ff Enable ESLint no-unused-expressions rule (#4363)
# Description of Changes
Enable ESLint [no-unused-expressions
rule](https://typescript-eslint.io/rules/no-unused-expressions/)
2025-09-04 15:12:38 +00:00
James BruntonandGitHub 74609e54fe Enable ESLint prefer-const rule (#4349)
# Description of Changes
Enable ESLint [prefer-const
rule](https://eslint.org/docs/latest/rules/prefer-const)
2025-09-04 15:09:29 +00:00
James BruntonandGitHub 003285506f Enable ESLint no-useless-escape rule (#4344)
# Description of Changes
Enable ESLint [no-useless-escape
rule](https://eslint.org/docs/latest/rules/no-useless-escape)
2025-09-04 15:04:49 +00:00
James BruntonandGitHub 6d3b08d9b6 Enable ESLint no-empty rule (#4342)
# Description of Changes
Enable ESLint [no-empty
rule](https://eslint.org/docs/latest/rules/no-empty)
2025-09-04 15:59:31 +01:00
James BruntonandGitHub 295e682e03 Add linting to frontend (#4341)
# Description of Changes
There's no current linter running over our TypeScript code, which means
we've got a bunch of dead code and other code smells around with nothing
notifying us. This PR adds ESLint with the typescript-eslint plugin and
enables the recommended settings as a starting point for us.

I've disabled all of the failing rules for the scope of this PR, just to
get linting running without causing a massive diff. I'll follow up with
future PRs that enable the failing rules one by one.

Also updates our version of TypeScript, which introduces a new type
error in the code (which I've had to fix)
2025-09-04 14:08:28 +01:00
1a3e8e7ecf Undo Button -> review state. Result files added to recents (#4337)
Produced PDFs go into recent files
Undo button added to review state
Undo causes undoConsume which replaces result files with source files.
Removes result files from recent files too

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-09-02 15:09:05 +01:00
96aa43860b Bugfix/V2/pdfa_extension (#4336)
Changes fallback pdfa extension pdf when a filename isnt returned for
convert.
Also fixes ui bug with footer and pop ups

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-09-01 12:23:27 +01:00
James BruntonandGitHub 6dbaff5342 Fix Split to be a single file interface (#4327)
# Description of Changes
Split was previously incorrectly marked as a multi-file interface, which
meant that if you fed 2 files into it, it'd just process the first and
discard the second.

This PR changes it to a single-file interface, and implements a custom
response handler because Split returns Zip files instead of PDFs, so the
response you get when running Split now is the union of all of the split
input files in the workbench (or them all zipped if you download it).
2025-08-29 17:23:44 +01:00
2f2f966ee9 Turn off logs from scarf (#4326)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-29 17:23:18 +01:00
James BruntonandGitHub 3b28b398d9 Automatically select added files (#4325)
# Description of Changes
When adding files, the user probably wants to use them straight away in
the next tool that they use, so automatically select any added files (in
addition to whatever they previously had selected).
2025-08-29 16:39:19 +01:00
eecc410b77 Basic footer structure and Cookie Consent (#4320)
* Added footer with blank links to be filled 
* Cookie consent to match V1 
* Made scrolling work on tool search results
* Made scrolling the same on tool search, tool picker and workbench 
* Cleaned up height variables, view height only used at workbench level 
<img width="1525" height="1270"
alt="{F3C1B15F-A4BE-4DF0-A5A8-92D2A3B14443}"
src="https://github.com/user-attachments/assets/0c23fe35-9973-45c0-85af-0002c5ff58d2"
/>
<img width="1511" height="1262"
alt="{4DDD51C0-4BC5-4E9F-A4F2-E5F49AF5F5FD}"
src="https://github.com/user-attachments/assets/2596d980-0312-4cd7-ad34-9fd3a8d1869e"
/>

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-08-29 13:01:46 +00:00
62c929b89b Top Controls was throwing an error (#4324)
<img width="337" height="76"
alt="{4B3307AF-6162-4648-BC06-D5E8967E5D1B}"
src="https://github.com/user-attachments/assets/6401da20-34a5-47f6-8d09-ce53211164d2"
/>

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-29 13:58:06 +01:00
James BruntonandGitHub 7c9769f5b9 Change to Viewer mode when Read tool selected (#4317)
# Description of Changes
Change it so that when you select the Read tool, the Viewer is
automatically selected.
2025-08-29 13:28:15 +01:00
James BruntonandGitHub cf771075f4 Reduce required gap between files so more fit on the screen (#4323)
# Description of Changes
Change gap between files in Active Files so more files fit on the screen
at once
2025-08-29 10:30:19 +01:00
a7d5c80188 Posthog, scarf and url navigation overhaul (#4318)
Added post hog project - always enabled
Added scarf pixel - Always enabled 
Reworked Url navigation 
Forward and back now works without reloading page

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-28 15:42:33 +01:00
James BruntonandGitHub 5b20f11e20 Add test that the translation JSON is valid (#4315)
# Description of Changes
Adds a vitest to check that the translation JSON files exist and are
actually valid JSON.
2025-08-28 11:03:43 +01:00
James BruntonandGitHub 442b373ff4 V2 reduce tool boilerplate (#4313)
# Description of Changes
Reduce boilerplate in tool frontends by creating a base frontend hook
for the simple tools to use.

I've done all the simple tools here. It'd be nice to add in some of the
more complex tools as well in the future if we can figure out how.
2025-08-28 09:59:38 +00:00
James BruntonandGitHub e142af2863 V2 Make FileId type opaque and use consistently throughout project (#4307)
# Description of Changes
The `FileId` type in V2 currently is just defined to be a string. This
makes it really easy to accidentally pass strings into things accepting
file IDs (such as file names). This PR makes the `FileId` type [an
opaque
type](https://www.geeksforgeeks.org/typescript/opaque-types-in-typescript/),
so it is compatible with things accepting strings (arguably not ideal
for this...) but strings are not compatible with it without explicit
conversion.

The PR also includes changes to use `FileId` consistently throughout the
project (everywhere I could find uses of `fileId: string`), so that we
have the maximum benefit from the type safety.

> [!note]
> I've marked quite a few things as `FIX ME` where we're passing names
in as IDs. If that is intended behaviour, I'm happy to remove the fix me
and insert a cast instead, but they probably need comments explaining
why we're using a file name as an ID.
2025-08-28 09:56:07 +00:00
581bafbd37 quick text changes (#4301)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-08-28 10:50:49 +01:00
James BruntonandGitHub 2cac8e8edf Redesgin ToolOperationConfig so types are known for single/multiple/custom tools (#4221)
# Description of Changes
Redesigns `ToolOperationConfig` so that the types of the functions are
always known depending on whether the tool runs on single files,
multiple files, or uses custom behaviour
2025-08-27 14:51:52 +01:00
James BruntonandGitHub 0002043266 Fix missing key React error in quick access bar (#4300)
# Description of Changes
React is currently throwing an error becasue of a missing key in V2.
This adds a key so it stops complaining.
2025-08-27 10:57:54 +01:00
James BruntonandGitHub 00f0194b32 Re-remove rainbow mode after mistaken revert (#4311)
# Description of Changes
Re-remove rainbow mode after mistaken revert
2025-08-27 09:32:39 +01:00
1c8b20b6e9 Split suggested fix (#4310)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 17:39:55 +01:00
ConnorYohandGitHub 1eee5c9a11 Remove config button (#4309) 2025-08-26 16:29:43 +00:00
68d59fd377 Feature/v2/page editor selection persistance (#4306)
Fixed page editor selection persistance
Fixed delete sleected
Fixed display issue on some tools on reset settings call

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-08-26 17:26:30 +01:00
47ccb6a6ed improvement/v2/automate/tweaks (#4293)
- [x] Cleanup Automation output name garbage			
- [x] Remove Cross button on first two tools			
- [x] Automation creation name title to make clearer to the user
- [x] Colours for dark mode on automation tool settings are bad 	
- [x] Fix tool names not using correct translated ones 
- [x] suggested Automation Password needs adding to description 
- [x] Allow different filetypes in automation
- [x] Custom Icons for automation
- [x] split Tool wasn't working with merge to single pdf

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-08-26 16:59:03 +01:00
3d26b054f1 V2 styling requests (#4288)
some of the requested changes warrant discussion so I haven't applied
all of them here. Making a draft PR for now, we can discuss the rest
later

# Description of Changes

See Matts todo list, the crossed out items are what were addressed in
this PR

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-08-26 16:31:20 +01:00
James BruntonandGitHub 9360a36c31 V2: Disable rainbow mode for demo (#4303)
# Description of Changes
The rainbow mode easter egg doesn't work very well at the moment and
isn't needed for the demo (and is too easy to accidentally enable). This
PR disables it completely by adding a global const to short-circuit the
activation code.
2025-08-26 14:56:37 +00:00
James BruntonandGitHub d64c56a897 Fix invalid translation file in V2 (#4298)
# Description of Changes
GB translations are currently invalid JSON. This fixes that.
2025-08-26 15:53:49 +01:00
ca423f9646 Feature/v2/pageeditor improved (#4289)
# Description of Changes

<!--
Please provide a summary of the changes, including:

Rewrite of page editor to make it work properly.  
Added page breaks
Added merged file support
Added "insert file" support
Slight Ux improvements

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-08-26 15:30:58 +01:00
Anthony StirlingandGitHub 9b8091a630 Revert "npm login" (#4299)
Reverts Stirling-Tools/Stirling-PDF#4296
2025-08-26 14:06:29 +01:00
4aac124e76 npm login (#4296)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
2025-08-26 13:46:15 +01:00
James BruntonandGitHub 6d7f76353e Add tooltips to advanced section in OCR and add missing translations (#4295)
# Description of Changes
Add tooltips to advanced section in OCR and add missing translations
2025-08-26 12:38:47 +01:00
EthanHealy01andGitHub 16e028a8ef Feature/v2/file manager UI improvements (#4286)
# Description of Changes

<img width="1171" height="416" alt="Screenshot 2025-08-26 at 1 33 14 AM"
src="https://github.com/user-attachments/assets/7fd132fb-93ae-4849-b7b0-78182707cf14"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-26 12:35:09 +01:00
95b3e22229 Automation tolltip + new operations + copy to saved (#4292)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-08-26 11:19:15 +01:00
4b70ef1298 Moved scroll out to tool panel and works better with zoom (#4282)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 09:12:15 +00:00
1cc803545a Dynamic upload/add files button for toolstep (#4284)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 10:09:49 +01:00
fe9d2367d5 AutomateFixes (#4281)
can edit automations
drop down styles
drop down bug fixes

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 09:44:30 +01:00
42d7664e25 Preview file (#4260)
Preview file works

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 09:26:26 +01:00
bbd658d3b8 Default View to file editor not page editor (#4258)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-26 09:25:33 +01:00
stirlingbot[bot]GitHubstirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
ed61c71db7 Update Frontend 3rd Party Licenses (#4254)
Auto-generated by stirlingbot[bot]

This PR updates the frontend license report based on changes to
package.json dependencies.

Signed-off-by: stirlingbot[bot] <stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-08-25 16:10:51 +01:00
73deece29e V2 Replace Google Fonts icons with locally bundled Iconify icons (#4283)
# Description of Changes

This PR refactors the frontend icon system to remove reliance on
@mui/icons-material and the Google Material Symbols webfont.

🔄 Changes

Introduced a new LocalIcon component powered by Iconify.
Added scripts/generate-icons.js to:
Scan the codebase for used icons.
Extract only required Material Symbols from
@iconify-json/material-symbols.
Generate a minimized JSON bundle and TypeScript types.
Updated .gitignore to exclude generated icon files.
Replaced all <span className="material-symbols-rounded"> and MUI icon
imports with <LocalIcon> usage.
Removed material-symbols CSS import and related font dependency.
Updated tsconfig.json to support JSON imports.
Added prebuild/predev hooks to auto-generate the icons.

✅ Benefits

No more 5MB+ Google webfont download → reduces initial page load size.
Smaller install footprint → no giant @mui/icons-material dependency.
Only ships the icons we actually use, cutting bundle size further.
Type-safe icons via auto-generated MaterialSymbolIcon union type.

Note most MUI not included in this update since they are low priority
due to small SVG sizing (don't grab whole bundle)


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
2025-08-25 16:07:55 +01:00
EthanHealy01andGitHub 55ebf9ebd0 Bug/v2/all tools section headers gap (#4275)
# Description of Changes

<img width="701" height="574" alt="Screenshot 2025-08-24 at 2 42 22 PM"
src="https://github.com/user-attachments/assets/a5b74eca-176d-4e34-b1da-c5d024a04896"
/>

to: 

<img width="731" height="542" alt="Screenshot 2025-08-24 at 2 42 41 PM"
src="https://github.com/user-attachments/assets/4e6b0632-5581-4e20-ba62-81ba2fafcb4a"
/>


---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-25 15:46:44 +01:00
e6f4cfb318 Automate/v2/suggested (#4257)
Suggested pipelines now work

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-08-25 13:10:13 +01:00
895bcebc7b Feature/v2/right rail (#4255)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-08-25 12:53:33 +01:00
EthanHealy01andGitHub 61f5221c58 Feature/v2/landing page upload buttons (#4259)
# Description of Changes

<img width="659" height="453" alt="Screenshot 2025-08-22 at 5 28 53 PM"
src="https://github.com/user-attachments/assets/1dd0f63c-0b08-435c-bb0e-00bd85be7f24"
/>
<img width="968" height="766" alt="Screenshot 2025-08-22 at 5 30 00 PM"
src="https://github.com/user-attachments/assets/86614256-1375-4e6e-b5bb-ae9cb6708135"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-25 12:43:20 +01:00
888bac9408 Added optional title for tool workflow (#4256)
- Added optional title for tool workflow - Not added to any tool. Just
there for when we need it
- Added add files button to files step
- renamed Local files button in filemanager to Upload Files
-

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
Co-authored-by: James Brunton <jbrunton96@gmail.com>
2025-08-22 17:12:14 +01:00
23d86deae7 Feature/v2/automate (#4248)
* automate feature
* Moved all providers to app level to simplify homepage 
* Circular dependency fixes
* You will see that now toolRegistry gets a tool config and a tool
settings object. These enable automate to run the tools using as much
static code as possible.

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-22 14:40:27 +01:00
James BruntonandGitHub 7d9c0b0298 V2 Fix subcategory names in All Tools (and search) pane (#4252)
# Description of Changes
Because we used string typing for IDs and names, it was really easy to
make mistakes where variables named like `subcategory` would be stored
as an ID in one file, but then read assuming it's a name in another
file. This PR changes the code to consistently use enum cases when
referring to IDs of categories, subcategories, and tools (at least in as
many places as I can find them, ~I had to add a `ToolId` enum for this
work~ I originally added a `ToolId` type for this work, but it caused
too many issues when merging with #4222 so I've pulled it back out for
now).

Making that change made it obvious where we were inconsistently passing
IDs and reading them as names etc. allowing me to fix rendering issues
in the All Tools pane, where the subcategory IDs were being rendered
directly (instead of being translated) or where IDs were being
translated into names, but were then being re-translated, causing
warnings in the log.
2025-08-22 13:53:06 +01:00
949ffa01ad Feature/v2/file handling improvements (#4222)
# Description of Changes

A new universal file context rather than the splintered ones for the
main views, tools and manager we had before (manager still has its own
but its better integreated with the core context)
File context has been split it into a handful of different files
managing various file related issues separately to reduce the monolith -
FileReducer.ts - State management
  fileActions.ts - File operations
  fileSelectors.ts - Data access patterns
  lifecycle.ts - Resource cleanup and memory management
  fileHooks.ts - React hooks interface
  contexts.ts - Context providers
Improved thumbnail generation
Improved indexxedb handling
Stopped handling files as blobs were not necessary to improve
performance
A new library handling drag and drop
https://github.com/atlassian/pragmatic-drag-and-drop (Out of scope yes
but I broke the old one with the new filecontext and it needed doing so
it was a might as well)
A new library handling virtualisation on page editor
@tanstack/react-virtual, as above.
Quickly ripped out the last remnants of the old URL params stuff and
replaced with the beginnings of what will later become the new URL
navigation system (for now it just restores the tool name in url
behavior)
Fixed selected file not regestered when opening a tool
Fixed png thumbnails
Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Reece Browne <you@example.com>
2025-08-21 17:30:26 +01:00
James BruntonandGitHub a33e51351b V2 hide incomplete tools (#4250)
# Description of Changes
Filters out tools which don't exist in All Tools list. Also fixes a
bunch of translation warnings in the log.
2025-08-21 09:26:17 +01:00
James BruntonandGitHub a6706fcb0c V2 reduce boilerplate in param hooks (#4246)
# Description of Changes
Extend the base params in all tools param hooks, reducing boilerplate
code.
2025-08-21 07:48:25 +00:00
d06cbcaa91 V2 Files Selected indicator in toolstep (#4241)
Added back our files selected indicator in tools step

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-20 15:54:30 +00:00
cd2b82d614 Feature/v2/filemanagerimprovements (#4243)
- Select all/deselect all
- Delete Selected
- Download  Selected
- Recent file delete -> menu button with drop down for delete and
download
- Shift click selection added
<img width="1220" height="751"
alt="{330DF96D-7040-4CCB-B089-523F370E3185}"
src="https://github.com/user-attachments/assets/976e42cc-2124-4e62-83a8-25f184e8da3b"
/>
<img width="1160" height="749"
alt="{2D2F4876-7D35-45C3-B0CD-3127EEEEF7B5}"
src="https://github.com/user-attachments/assets/6879a174-a135-41f4-a876-984e7c2f96e2"
/>

---------

Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-08-20 16:51:55 +01:00
Anthony StirlingandGitHub eada9e43ec tooltip (#4242)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-20 14:12:12 +01:00
Anthony StirlingandGitHub d1cb3f0b30 V2 PDF to large page (#4236)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-20 11:38:41 +01:00
Anthony StirlingandGitHub 857b2b5c53 V2 Unlock forms (#4237)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-20 11:34:28 +01:00
Anthony StirlingandGitHub d74c3f4598 V2 remove cert (#4239)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-08-20 11:30:57 +01:00
371 changed files with 36689 additions and 11029 deletions
+7 -3
View File
@@ -11,8 +11,12 @@
"Bash(npm test:*)",
"Bash(ls:*)",
"Bash(npx tsc:*)",
"Bash(sed:*)"
"Bash(node:*)",
"Bash(npm run dev:*)",
"Bash(sed:*)",
"Bash(npm run typecheck:*)"
],
"deny": []
"deny": [],
"defaultMode": "acceptEdits"
}
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
[{*.js,*.jsx,*.ts,*.tsx}]
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
indent_size = 2
[*.css]
+2
View File
@@ -147,6 +147,8 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Lint frontend
run: cd frontend && npm run lint
- name: Build frontend
run: cd frontend && npm run build
- name: Run frontend tests
+1
View File
@@ -19,5 +19,6 @@
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
]
}
+4 -1
View File
@@ -139,5 +139,8 @@
"app/core/src/main/java",
"app/common/src/main/java",
"app/proprietary/src/main/java"
]
],
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}
+300
View File
@@ -0,0 +1,300 @@
# Adding New React Tools to Stirling PDF
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
## Overview
When adding tools, follow this systematic approach using the established patterns and architecture.
## 1. Create Tool Structure
Create these files in the correct directories:
```
frontend/src/hooks/tools/[toolName]/
├── use[ToolName]Parameters.ts # Parameter definitions and validation
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
frontend/src/components/tools/[toolName]/
└── [ToolName]Settings.tsx # Settings UI component (if needed)
frontend/src/tools/
└── [ToolName].tsx # Main tool component
```
## 2. Implementation Pattern
Use `useBaseTool` for simplified hook management. This is the recommended approach for all new tools:
**Parameters Hook** (`use[ToolName]Parameters.ts`):
```typescript
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface [ToolName]Parameters extends BaseParameters {
// Define your tool-specific parameters here
someOption: boolean;
}
export const defaultParameters: [ToolName]Parameters = {
someOption: false,
};
export const use[ToolName]Parameters = (): BaseParametersHook<[ToolName]Parameters> => {
return useBaseParameters({
defaultParameters,
endpointName: 'your-endpoint-name',
validateFn: (params) => true, // Add validation logic
});
};
```
**Operation Hook** (`use[ToolName]Operation.ts`):
```typescript
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
export const build[ToolName]FormData = (parameters: [ToolName]Parameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
// Add parameters to formData
return formData;
};
export const [toolName]OperationConfig = {
toolType: ToolType.singleFile, // or ToolType.multiFile (buildFormData's file parameter will need to be updated)
buildFormData: build[ToolName]FormData,
operationType: '[toolName]',
endpoint: '/api/v1/category/endpoint-name',
filePrefix: 'processed_', // Will be overridden with translation
defaultParameters,
} as const;
export const use[ToolName]Operation = () => {
const { t } = useTranslation();
return useToolOperation({
...[toolName]OperationConfig,
filePrefix: t('[toolName].filenamePrefix', 'processed') + '_',
getErrorMessage: createStandardErrorHandler(t('[toolName].error.failed', 'Operation failed'))
});
};
```
**Main Component** (`[ToolName].tsx`):
```typescript
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { use[ToolName]Parameters } from "../hooks/tools/[toolName]/use[ToolName]Parameters";
import { use[ToolName]Operation } from "../hooks/tools/[toolName]/use[ToolName]Operation";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
const [ToolName] = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool('[toolName]', use[ToolName]Parameters, use[ToolName]Operation, props);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
placeholder: t("[toolName].files.placeholder", "Select files to get started"),
},
steps: [
// Add settings steps if needed
],
executeButton: {
text: t("[toolName].submit", "Process"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("[toolName].results.title", "Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
[ToolName].tool = () => use[ToolName]Operation;
export default [ToolName] as ToolComponent;
```
**Note**: Some existing tools (like AddPassword, Compress) use a legacy pattern with manual hook management. **Always use the Modern Pattern above for new tools** - it's cleaner, more maintainable, and includes automation support.
## 3. Register Tool in System
Update these files to register your new tool:
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
1. Add imports at the top:
```typescript
import [ToolName] from "../tools/[ToolName]";
import { [toolName]OperationConfig } from "../hooks/tools/[toolName]/use[ToolName]Operation";
import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Settings";
```
2. Add tool entry in the `allTools` object:
```typescript
[toolName]: {
icon: <LocalIcon icon="your-icon-name" width="1.5rem" height="1.5rem" />,
name: t("home.[toolName].title", "Tool Name"),
component: [ToolName],
description: t("home.[toolName].desc", "Tool description"),
categoryId: ToolCategoryId.STANDARD_TOOLS, // or appropriate category
subcategoryId: SubcategoryId.APPROPRIATE_SUBCATEGORY,
maxFiles: -1, // or specific number
endpoints: ["endpoint-name"],
operationConfig: [toolName]OperationConfig,
settingsComponent: [ToolName]Settings, // if settings exist
},
```
## 4. Add Tooltips (Optional but Recommended)
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
```typescript
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const use[ToolName]Tips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("[toolName].tooltip.header.title", "Tool Overview")
},
tips: [
{
title: t("[toolName].tooltip.description.title", "What does this tool do?"),
description: t("[toolName].tooltip.description.text", "Simple explanation in everyday language that non-technical users can understand."),
bullets: [
t("[toolName].tooltip.description.bullet1", "Easy-to-understand benefit 1"),
t("[toolName].tooltip.description.bullet2", "Easy-to-understand benefit 2")
]
}
// Add more tip sections as needed
]
};
};
```
**Add tooltip to your main component:**
```typescript
import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
const [ToolName] = (props: BaseToolProps) => {
const tips = use[ToolName]Tips();
// In your steps array:
steps: [
{
title: t("[toolName].steps.settings", "Settings"),
tooltip: tips, // Add this line
content: <[ToolName]Settings ... />
}
]
```
## 5. Add Translations
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
**File to update:** `frontend/public/locales/en-GB/translation.json`
**Required Translation Keys**:
```json
{
"home": {
"[toolName]": {
"title": "Tool Name",
"desc": "Tool description"
}
},
"[toolName]": {
"title": "Tool Name",
"submit": "Process",
"filenamePrefix": "processed",
"files": {
"placeholder": "Select files to get started"
},
"steps": {
"settings": "Settings"
},
"options": {
"title": "Tool Options",
"someOption": "Option Label",
"someOption.desc": "Option description",
"note": "General information about the tool."
},
"results": {
"title": "Results"
},
"error": {
"failed": "Operation failed"
},
"tooltip": {
"header": {
"title": "Tool Overview"
},
"description": {
"title": "What does this tool do?",
"text": "Simple explanation in everyday language",
"bullet1": "Easy-to-understand benefit 1",
"bullet2": "Easy-to-understand benefit 2"
}
}
}
}
```
**Translation Notes:**
- **Only update `en-GB/translation.json`** - other locale files are managed separately
- Use descriptive keys that match your component's `t()` calls
- Include tooltip translations if you created tooltip hooks
- Add `options.*` keys if your tool has settings with descriptions
**Tooltip Writing Guidelines:**
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
- **Focus on benefits** - explain what the user gains, not how it works internally
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
- **Keep descriptions concise** - 1-2 sentences maximum per section
- **Use bullet points** for multiple benefits or features
## 6. Migration from Thymeleaf
When migrating existing Thymeleaf templates:
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
## 7. Testing Your Tool
- Verify tool appears in UI with correct icon and description
- Test with various file sizes and types
- Confirm translations work
- Check error handling
- Test undo functionality
- Verify results display correctly
## Tool Development Patterns
### Three Tool Patterns:
**Pattern 1: Single-File Tools** (Individual processing)
- Backend processes one file per API call
- Set `multiFileEndpoint: false`
- Examples: Compress, Rotate
**Pattern 2: Multi-File Tools** (Batch processing)
- Backend accepts `MultipartFile[]` arrays in single API call
- Set `multiFileEndpoint: true`
- Examples: Split, Merge, Overlay
**Pattern 3: Complex Tools** (Custom processing)
- Tools with complex routing logic or non-standard processing
- Provide `customProcessor` for full control
- Examples: Convert, OCR
+1
View File
@@ -208,6 +208,7 @@ return useToolOperation({
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
## Communication Style
- Be direct and to the point
+319
View File
@@ -0,0 +1,319 @@
# Stirling PDF File History Specification
## Overview
Stirling PDF implements a client-side file history system using IndexedDB storage. File metadata, including version history and tool chains, are stored as `StirlingFileStub` objects that travel alongside the actual file data. This enables comprehensive version tracking, tool history, and file lineage management without modifying PDF content.
## Storage Architecture
### IndexedDB-Based Storage
File history is stored in the browser's IndexedDB using the `fileStorage` service, providing:
- **Persistent storage**: Survives browser sessions and page reloads
- **Large capacity**: Supports files up to 100GB+ with full metadata
- **Fast queries**: Optimized for file browsing and history lookups
- **Type safety**: Structured TypeScript interfaces
### Core Data Structures
```typescript
interface StirlingFileStub extends BaseFileMetadata {
id: FileId; // Unique file identifier (UUID)
quickKey: string; // Deduplication key: name|size|lastModified
thumbnailUrl?: string; // Generated thumbnail blob URL
processedFile?: ProcessedFileMetadata; // PDF page data and processing results
// File Metadata
name: string;
size: number;
type: string;
lastModified: number;
createdAt: number;
// Version Control
isLeaf: boolean; // True if this is the latest version
versionNumber?: number; // Version number (1, 2, 3, etc.)
originalFileId?: string; // UUID of the root file in version chain
parentFileId?: string; // UUID of immediate parent file
// Tool History
toolHistory?: ToolOperation[]; // Complete sequence of applied tools
}
interface ToolOperation {
toolName: string; // Tool identifier (e.g., 'compress', 'sanitize')
timestamp: number; // When the tool was applied
}
interface StoredStirlingFileRecord extends StirlingFileStub {
data: ArrayBuffer; // Actual file content
fileId: FileId; // Duplicate for indexing
}
```
## Version Management System
### Version Progression
- **v1**: Original uploaded file (first version)
- **v2**: First tool applied to original
- **v3**: Second tool applied (inherits from v2)
- **v4**: Third tool applied (inherits from v3)
- **etc.**
### Leaf Node System
Only the latest version of each file family is marked as `isLeaf: true`:
- **Leaf files**: Show in default file list, available for tool processing
- **History files**: Hidden by default, accessible via history expansion
### File Relationships
```
document.pdf (v1, isLeaf: false)
↓ compress
document.pdf (v2, isLeaf: false)
↓ sanitize
document.pdf (v3, isLeaf: true) ← Current active version
```
## Implementation Architecture
### 1. FileStorage Service (`fileStorage.ts`)
**Core Methods:**
```typescript
// Store file with complete metadata
async storeStirlingFile(stirlingFile: StirlingFile, stub: StirlingFileStub): Promise<void>
// Load file with metadata
async getStirlingFile(id: FileId): Promise<StirlingFile | null>
async getStirlingFileStub(id: FileId): Promise<StirlingFileStub | null>
// Query operations
async getLeafStirlingFileStubs(): Promise<StirlingFileStub[]>
async getAllStirlingFileStubs(): Promise<StirlingFileStub[]>
// Version management
async markFileAsProcessed(fileId: FileId): Promise<boolean> // Set isLeaf = false
async markFileAsLeaf(fileId: FileId): Promise<boolean> // Set isLeaf = true
```
### 2. File Context Integration
**FileContext** manages runtime state with `StirlingFileStub[]` in memory:
```typescript
interface FileContextState {
files: {
ids: FileId[];
byId: Record<FileId, StirlingFileStub>;
};
}
```
**Key Operations:**
- `addFiles()`: Stores new files with initial metadata
- `addStirlingFileStubs()`: Loads existing files from storage with preserved metadata
- `consumeFiles()`: Processes files through tools, creating new versions
### 3. Tool Operation Integration
**Tool Processing Flow:**
1. **Input**: User selects files (marked as `isLeaf: true`)
2. **Processing**: Backend processes files and returns results
3. **History Creation**: New `StirlingFileStub` created with:
- Incremented version number
- Updated tool history
- Parent file reference
4. **Storage**: Both parent (marked `isLeaf: false`) and child (marked `isLeaf: true`) stored
5. **UI Update**: FileContext updated with new file state
**Child Stub Creation:**
```typescript
export function createChildStub(
parentStub: StirlingFileStub,
operation: { toolName: string; timestamp: number },
resultingFile: File,
thumbnail?: string
): StirlingFileStub {
return {
id: createFileId(),
name: resultingFile.name,
size: resultingFile.size,
type: resultingFile.type,
lastModified: resultingFile.lastModified,
quickKey: createQuickKey(resultingFile),
createdAt: Date.now(),
isLeaf: true,
// Version Control
versionNumber: (parentStub.versionNumber || 1) + 1,
originalFileId: parentStub.originalFileId || parentStub.id,
parentFileId: parentStub.id,
// Tool History
toolHistory: [...(parentStub.toolHistory || []), operation],
thumbnailUrl: thumbnail
};
}
```
## UI Integration
### File Manager History Display
**FileManager** (`FileManager.tsx`) provides:
- **Default View**: Shows only leaf files (`isLeaf: true`)
- **History Expansion**: Click to show all versions of a file family
- **History Groups**: Nested display using `FileHistoryGroup.tsx`
**FileListItem** (`FileListItem.tsx`) displays:
- **Version Badges**: v1, v2, v3 indicators
- **Tool Chain**: Complete processing history in tooltips
- **History Actions**: "Show/Hide History" toggle, "Restore" for history files
### FileManagerContext Integration
**File Selection Flow:**
```typescript
// Recent files (from storage)
onRecentFileSelect: (stirlingFileStubs: StirlingFileStub[]) => void
// Calls: actions.addStirlingFileStubs(stirlingFileStubs, options)
// New uploads
onFileUpload: (files: File[]) => void
// Calls: actions.addFiles(files, options)
```
**History Management:**
```typescript
// Toggle history visibility
const { expandedFileIds, onToggleExpansion } = useFileManagerContext();
// Restore history file to current
const handleAddToRecents = (file: StirlingFileStub) => {
fileStorage.markFileAsLeaf(file.id); // Make this version current
};
```
## Data Flow
### New File Upload
```
1. User uploads files → addFiles()
2. Generate thumbnails and page count
3. Create StirlingFileStub with isLeaf: true, versionNumber: 1
4. Store both StirlingFile + StirlingFileStub in IndexedDB
5. Dispatch to FileContext state
```
### Tool Processing
```
1. User selects tool + files → useToolOperation()
2. API processes files → returns processed File objects
3. createChildStub() for each result:
- Parent marked isLeaf: false
- Child created with isLeaf: true, incremented version
4. Store all files with updated metadata
5. Update FileContext with new state
```
### File Loading (Recent Files)
```
1. User selects from FileManager → onRecentFileSelect()
2. addStirlingFileStubs() with preserved metadata
3. Load actual StirlingFile data from storage
4. Files appear in workbench with complete history intact
```
## Performance Optimizations
### Metadata Regeneration
When loading files from storage, missing `processedFile` data is regenerated:
```typescript
// In addStirlingFileStubs()
const needsProcessing = !record.processedFile ||
!record.processedFile.pages ||
record.processedFile.pages.length === 0;
if (needsProcessing) {
const result = await generateThumbnailWithMetadata(stirlingFile);
record.processedFile = createProcessedFile(result.pageCount, result.thumbnail);
}
```
### Memory Management
- **Blob URL Tracking**: Automatic cleanup of thumbnail URLs
- **Lazy Loading**: Files loaded from storage only when needed
- **LRU Caching**: File objects cached in memory with size limits
## File Deduplication
### QuickKey System
Files are deduplicated using `quickKey` format:
```typescript
const quickKey = `${file.name}|${file.size}|${file.lastModified}`;
```
This prevents duplicate uploads while allowing different versions of the same logical file.
## Error Handling
### Graceful Degradation
- **Storage Failures**: Files continue to work without persistence
- **Metadata Issues**: Missing metadata regenerated on demand
- **Version Conflicts**: Automatic version number resolution
### Recovery Scenarios
- **Corrupted Storage**: Automatic cleanup and re-initialization
- **Missing Files**: Stubs cleaned up automatically
- **Version Mismatches**: Automatic version chain reconstruction
## Developer Guidelines
### Adding File History to New Components
1. **Use FileContext Actions**:
```typescript
const { actions } = useFileActions();
await actions.addFiles(files); // For new uploads
await actions.addStirlingFileStubs(stubs); // For existing files
```
2. **Preserve Metadata When Processing**:
```typescript
const childStub = createChildStub(parentStub, {
toolName: 'compress',
timestamp: Date.now()
}, processedFile, thumbnail);
```
3. **Handle Storage Operations**:
```typescript
await fileStorage.storeStirlingFile(stirlingFile, stirlingFileStub);
const stub = await fileStorage.getStirlingFileStub(fileId);
```
### Testing File History
1. **Upload files**: Should show v1, marked as leaf
2. **Apply tool**: Should create v2, mark v1 as non-leaf
3. **Check FileManager**: History should show both versions
4. **Restore old version**: Should mark old version as leaf
5. **Check storage**: Both versions should persist in IndexedDB
## Future Enhancements
### Potential Improvements
- **Branch History**: Support for parallel processing branches
- **History Export**: Export complete version history as JSON
- **Conflict Resolution**: Handle concurrent modifications
- **Cloud Sync**: Sync history across devices
- **Compression**: Compress historical file data
### API Extensions
- **Batch Operations**: Process multiple version chains simultaneously
- **Search Integration**: Search within tool history and file metadata
- **Analytics**: Track usage patterns and tool effectiveness
---
**Last Updated**: January 2025
**Implementation**: Stirling PDF Frontend v2
**Storage Version**: IndexedDB with fileStorage service
+5 -1
View File
@@ -24,4 +24,8 @@ yarn-debug.log*
yarn-error.log*
playwright-report
test-results
test-results
# auto-generated files
/src/assets/material-symbols-icons.json
/src/assets/material-symbols-icons.d.ts
+42
View File
@@ -0,0 +1,42 @@
// @ts-check
import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
{
ignores: [
"dist", // Contains 3rd party code
"public", // Contains 3rd party code
],
},
{
rules: {
"no-undef": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-empty-object-type": [
"error",
{
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
allowInterfaces: 'with-single-extends',
},
],
"@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "all", // All function args must be used (or explicitly ignored)
"argsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
"caughtErrors": "all", // Caught errors must be used (or explicitly ignored)
"caughtErrorsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
"destructuredArrayIgnorePattern": "^_", // Allow unused variables beginning with an underscore
"varsIgnorePattern": "^_", // Allow unused variables beginning with an underscore
"ignoreRestSiblings": true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
},
],
},
}
);
+2576 -28
View File
File diff suppressed because it is too large Load Diff
+21 -4
View File
@@ -5,14 +5,17 @@
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
"proxy": "http://localhost:8080",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@iconify/react": "^6.0.0",
"@mantine/core": "^8.0.1",
"@mantine/dropzone": "^8.0.1",
"@mantine/hooks": "^8.0.1",
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@tailwindcss/postcss": "^4.1.8",
"@tanstack/react-virtual": "^3.13.12",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -23,9 +26,9 @@
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"jszip": "^3.10.1",
"material-symbols": "^0.33.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"posthog-js": "^1.261.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.2",
@@ -34,11 +37,19 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"dev": "npx tsc --noEmit && vite",
"build": "npx tsc --noEmit && vite build",
"predev": "npm run generate-icons",
"dev": "npm run typecheck && vite",
"prebuild": "npm run generate-icons",
"lint": "eslint",
"build": "npm run typecheck && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"check": "npm run typecheck && npm run lint && npm run test:run",
"generate-licenses": "node scripts/generate-licenses.js",
"generate-icons": "node scripts/generate-icons.js",
"generate-icons:verbose": "node scripts/generate-icons.js --verbose",
"test": "vitest",
"test:run": "vitest run",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
@@ -64,19 +75,25 @@
]
},
"devDependencies": {
"@eslint/js": "^9.34.0",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify/utils": "^3.0.1",
"@playwright/test": "^1.40.0",
"@types/node": "^24.2.1",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^1.0.0",
"eslint": "^9.34.0",
"jsdom": "^23.0.0",
"license-checker": "^25.0.1",
"madge": "^8.0.0",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "^5.8.3",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vite": "^6.3.5",
"vitest": "^1.0.0"
}
@@ -1,4 +1,4 @@
<svg width="146" height="157" viewBox="0 0 146 157" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.77397 72.5462L93.6741 23.0462L94.7739 70.0462L3.77395 119.046L3.77397 72.5462Z" fill="#E6E6E6" fill-opacity="0.4"/>
<path d="M50.774 73.5735L96.387 50.2673L142 26.961L142 71.687L50.7739 122.046L50.774 73.5735Z" fill="#E6E6E6" fill-opacity="0.7"/>
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#E6E6E6" fill-opacity="0.4"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#E6E6E6" fill-opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 253 B

@@ -1,4 +1,4 @@
<svg width="146" height="146" viewBox="0 0 146 146" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.77397 72.5462L93.6741 23.0462L94.7739 70.0462L3.77395 119.046L3.77397 72.5462Z" fill="#ACACAC" fill-opacity="0.3"/>
<path d="M50.774 73.5735L96.387 50.2673L142 26.961L142 71.687L50.7739 122.046L50.774 73.5735Z" fill="#FC9999" fill-opacity="0.5"/>
<svg width="92" height="100" viewBox="0 0 92 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 50L60 0V46.5L0 96.5V50Z" fill="#ACACAC" fill-opacity="0.3"/>
<path d="M32 53L92 3V49.5L32 99.5V53Z" fill="#FC9999" fill-opacity="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 253 B

File diff suppressed because one or more lines are too long
@@ -0,0 +1,178 @@
/* Light theme variables */
:root {
--cc-bg: #ffffff;
--cc-primary-color: #1c1c1c;
--cc-secondary-color: #666666;
--cc-btn-primary-bg: #007BFF;
--cc-btn-primary-color: #ffffff;
--cc-btn-primary-border-color: #007BFF;
--cc-btn-primary-hover-bg: #0056b3;
--cc-btn-primary-hover-color: #ffffff;
--cc-btn-primary-hover-border-color: #0056b3;
--cc-btn-secondary-bg: #f1f3f4;
--cc-btn-secondary-color: #1c1c1c;
--cc-btn-secondary-border-color: #f1f3f4;
--cc-btn-secondary-hover-bg: #007BFF;
--cc-btn-secondary-hover-color: #ffffff;
--cc-btn-secondary-hover-border-color: #007BFF;
--cc-separator-border-color: #e0e0e0;
--cc-toggle-on-bg: #007BFF;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #ffffff;
--cc-toggle-off-knob-bg: #ffffff;
--cc-toggle-enabled-icon-color: #ffffff;
--cc-toggle-disabled-icon-color: #ffffff;
--cc-toggle-readonly-bg: #f1f3f4;
--cc-toggle-readonly-knob-bg: #79747E;
--cc-toggle-readonly-knob-icon-color: #f1f3f4;
--cc-section-category-border: #e0e0e0;
--cc-cookie-category-block-bg: #f1f3f4;
--cc-cookie-category-block-border: #f1f3f4;
--cc-cookie-category-block-hover-bg: #e9eff4;
--cc-cookie-category-block-hover-border: #e9eff4;
--cc-cookie-category-expanded-block-bg: #f1f3f4;
--cc-cookie-category-expanded-block-hover-bg: #e9eff4;
--cc-footer-bg: #ffffff;
--cc-footer-color: #1c1c1c;
--cc-footer-border-color: #ffffff;
}
/* Dark theme variables */
.cc--darkmode{
--cc-bg: #2d2d2d;
--cc-primary-color: #e5e5e5;
--cc-secondary-color: #b0b0b0;
--cc-btn-primary-bg: #4dabf7;
--cc-btn-primary-color: #2d2d2d;
--cc-btn-primary-border-color: #4dabf7;
--cc-btn-primary-hover-bg: #3d3d3d;
--cc-btn-primary-hover-color: #e5e5e5;
--cc-btn-primary-hover-border-color: #3d3d3d;
--cc-btn-secondary-bg: #3d3d3d;
--cc-btn-secondary-color: #e5e5e5;
--cc-btn-secondary-border-color: #3d3d3d;
--cc-btn-secondary-hover-bg: #4dabf7;
--cc-btn-secondary-hover-color: #2d2d2d;
--cc-btn-secondary-hover-border-color: #4dabf7;
--cc-separator-border-color: #555555;
--cc-toggle-on-bg: #4dabf7;
--cc-toggle-off-bg: #667481;
--cc-toggle-on-knob-bg: #2d2d2d;
--cc-toggle-off-knob-bg: #2d2d2d;
--cc-toggle-enabled-icon-color: #2d2d2d;
--cc-toggle-disabled-icon-color: #2d2d2d;
--cc-toggle-readonly-bg: #555555;
--cc-toggle-readonly-knob-bg: #8e8e8e;
--cc-toggle-readonly-knob-icon-color: #555555;
--cc-section-category-border: #555555;
--cc-cookie-category-block-bg: #3d3d3d;
--cc-cookie-category-block-border: #3d3d3d;
--cc-cookie-category-block-hover-bg: #4d4d4d;
--cc-cookie-category-block-hover-border: #4d4d4d;
--cc-cookie-category-expanded-block-bg: #3d3d3d;
--cc-cookie-category-expanded-block-hover-bg: #4d4d4d;
--cc-footer-bg: #2d2d2d;
--cc-footer-color: #e5e5e5;
--cc-footer-border-color: #2d2d2d;
}
.cm__body{
max-width: 90% !important;
flex-direction: row !important;
align-items: center !important;
}
.cm__desc{
max-width: 70rem !important;
}
.cm__btns{
flex-direction: row-reverse !important;
gap:10px !important;
padding-top: 3.4rem !important;
}
@media only screen and (max-width: 1400px) {
.cm__body{
max-width: 90% !important;
flex-direction: column !important;
align-items: normal !important;
}
.cm__btns{
padding-top: 1rem !important;
}
}
/* Toggle visibility fixes */
#cc-main .section__toggle {
opacity: 0 !important; /* Keep invisible but functional */
}
#cc-main .toggle__icon {
display: flex !important;
align-items: center !important;
justify-content: flex-start !important;
}
#cc-main .toggle__icon-circle {
display: block !important;
position: absolute !important;
transition: transform 0.25s ease !important;
}
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
display: flex !important;
align-items: center !important;
justify-content: center !important;
position: absolute !important;
width: 100% !important;
height: 100% !important;
}
/* Ensure toggles are visible in both themes */
#cc-main .toggle__icon {
background: var(--cc-toggle-off-bg) !important;
border: 1px solid var(--cc-toggle-off-bg) !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
background: var(--cc-toggle-on-bg) !important;
border: 1px solid var(--cc-toggle-on-bg) !important;
}
/* Ensure toggle text is visible */
#cc-main .pm__section-title {
color: var(--cc-primary-color) !important;
}
#cc-main .pm__section-desc {
color: var(--cc-secondary-color) !important;
}
/* Make sure the modal has proper contrast */
#cc-main .pm {
background: var(--cc-bg) !important;
color: var(--cc-primary-color) !important;
}
File diff suppressed because one or more lines are too long
+203 -18
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "جارٍ التحميل...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "إستبدال و عكس الألوان",
"desc": "استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف"
},
"convert": {
"title": "تحويل"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "استخراج الصفحات"
},
"removePages": {
"title": "إزالة الصفحات",
"desc": "حذف الصفحات غير المرغوب فيها من مستند PDF الخاص بك."
},
"removeImagePdf": {
"title": "إزالة الصورة",
"desc": "إزالة الصورة من PDF لتقليل حجم الملف"
},
"autoSizeSplitPDF": {
"title": "تقسيم تلقائي حسب الحجم/العدد",
"desc": "تقسيم ملف PDF واحد إلى مستندات متعددة بناءً على الحجم أو عدد الصفحات أو عدد المستندات"
},
"adjust-contrast": {
"title": "ضبط الألوان/التباين",
"desc": "ضبط التباين والتشبع والسطوع لملف PDF"
},
"replaceColorPdf": {
"title": "إستبدال و عكس الألوان",
"desc": "استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف"
},
"changePermissions": {
"title": "تغيير الأذونات"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "الترتيب حسب الاسم",
"sortByDate": "الترتيب حسب التاريخ",
"removeCertSign": "إزالة التوقيع الرقمي في الملف المدمج؟",
"submit": "دمج"
"submit": "دمج",
"sortBy": {
"filename": "اسم الملف"
}
},
"split": {
"tags": "عمليات الصفحة,تقسيم,صفحات متعددة,قص,جانب الخادم",
@@ -634,7 +669,20 @@
"8": "المستند رقم 6: الصفحة 10"
},
"splitPages": "أدخل الصفحات المراد تقسيمها:",
"submit": "تقسيم"
"submit": "تقسيم",
"steps": {
"settings": "إعدادات"
},
"methods": {
"bySize": {
"name": "حجم الملف"
}
},
"value": {
"fileSize": {
"label": "حجم الملف"
}
}
},
"rotate": {
"tags": "جانب الخادم",
@@ -712,6 +760,12 @@
"type": {
"1": "نص",
"2": "صورة"
},
"watermarkType": {
"text": "نص"
},
"settings": {
"fontSize": "حجم الخط"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "تغيير"
},
"removePages": {
"tags": "إزالة الصفحات,حذف الصفحات"
"tags": "إزالة الصفحات,حذف الصفحات",
"title": "إزالة الصفحات",
"submit": "إزالة الصفحات"
},
"addPassword": {
"tags": "تأمين,أمان",
@@ -758,7 +814,12 @@
"15": "يقيد ما يمكن القيام به بالمستند بمجرد فتحه (غير مدعوم من قبل جميع القراء)",
"16": "يقيد فتح المستند نفسه"
},
"submit": "تشفير"
"submit": "تشفير",
"tooltip": {
"permissions": {
"title": "تغيير الأذونات"
}
}
},
"removePassword": {
"tags": "تأمين,فك التشفير,أمان,إزالة كلمة المرور,حذف كلمة المرور",
@@ -768,7 +829,12 @@
"1": "حدد PDF لفك التشفير",
"2": "كلمة المرور"
},
"submit": "إزالة"
"submit": "إزالة",
"desc": "إزالة الحماية بكلمة مرور من مستند PDF الخاص بك.",
"password": {
"stepTitle": "إزالة كلمة المرور",
"label": "كلمة المرور الحالية"
}
},
"compressPdfs": {
"tags": "ضغط,صغير,ضئيل"
@@ -829,7 +895,25 @@
},
"help": "يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker",
"credit": "تستخدم هذه الخدمة qpdf و Tesseract للتعرف الضوئي على الحروف.",
"submit": "معالجة PDF باستخدام OCR"
"submit": "معالجة PDF باستخدام OCR",
"desc": "يقوم برنامج التنظيف بمسح واكتشاف النص من الصور داخل ملف PDF ويعيد إضافته كنص",
"settings": {
"title": "إعدادات",
"ocrMode": {
"label": "وضع التعرف الضوئي على الحروف"
},
"languages": {
"label": "اللغات"
}
},
"tooltip": {
"mode": {
"title": "وضع التعرف الضوئي على الحروف"
},
"languages": {
"title": "اللغات"
}
}
},
"extractImages": {
"tags": "صورة,صورة فوتوغرافية,حفظ,أرشيف,ملف مضغوط,التقاط,انتزاع",
@@ -924,7 +1008,7 @@
"sharedSigs": "توقيعات مشتركة",
"noSavedSigs": "لم يتم العثور على توقيعات محفوظة",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "حذف",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "تسطيح",
"header": "تسطيح ملفات PDF",
"flattenOnlyForms": "تسطيح النماذج فقط",
"submit": "تسطيح"
"submit": "تسطيح",
"steps": {
"settings": "إعدادات"
},
"options": {
"flattenOnlyForms": "تسطيح النماذج فقط"
}
},
"repair": {
"tags": "إصلاح,استعادة,تصحيح,استرداد",
@@ -1106,10 +1196,10 @@
"credit": "يستخدم WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "لغة الترميز,محتوى الويب,تحويل,تحويل",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "تحويل"
},
"getPdfInfo": {
"tags": "معلومات,بيانات,إحصائيات,إحصاءات",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,22 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "متقدم"
},
"wordsToRedact": {
"add": "إضافة"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "صفحات",
"placeholder": "(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)"
},
"export": "تصدير"
}
},
"tableExtraxt": {
"tags": "CSV,استخراج الجدول,استخراج,تحويل"
@@ -1261,8 +1366,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "السبب",
"location": "الموقع",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1386,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "الإصدار",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1491,10 @@
"4": "الوضع التلقائي - يضبط الجودة تلقائيًا للحصول على ملف PDF بالحجم المحدد",
"5": "حجم PDF المتوقع (على سبيل المثال 25 ميجا بايت، 10.8 ميجا بايت، 25 كيلو بايت)"
},
"submit": "ضغط"
"submit": "ضغط",
"method": {
"filesize": "حجم الملف"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "تنزيل",
"convert": {
"title": "تحويل",
"settings": "إعدادات",
"color": "لون",
"greyscale": "تدرج الرمادي",
"fillPage": "ملء الصفحة",
"pdfaDigitalSignatureWarning": "يحتوي PDF على توقيع رقمي. سيتم إزالة هذا في الخطوة التالية.",
"grayscale": "تدرج الرمادي"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "تحديد الكل",
"deselectAll": "إلغاء تحديد الكل"
},
"quickAccess": {
"sign": "توقيع"
},
"fileUpload": {
"loading": "جارٍ التحميل...",
"or": "أو"
},
"fileManager": {
"fileName": "الاسم",
"fileVersion": "الإصدار",
"selectAll": "تحديد الكل",
"deselectAll": "إلغاء تحديد الكل",
"deleteSelected": "حذف المحدد",
"download": "تنزيل",
"delete": "حذف"
},
"sanitize": {
"submit": "تنظيف PDF",
"steps": {
"settings": "إعدادات"
}
},
"changePermissions": {
"title": "تغيير الأذونات",
"submit": "تغيير الأذونات",
"permissions": {
"preventAssembly": {
"label": "منع تجميع المستند"
},
"preventExtractContent": {
"label": "منع استخراج المحتوى"
},
"preventExtractForAccessibility": {
"label": "منع الاستخراج للوصول"
},
"preventFillInForm": {
"label": "منع ملء النموذج"
},
"preventModify": {
"label": "منع التعديل"
},
"preventModifyAnnotations": {
"label": "منع تعديل التعليق التوضيحي"
},
"preventPrinting": {
"label": "منع الطباعة"
},
"preventPrintingFaithful": {
"label": "منع طباعة التنسيقات المختلفة"
}
},
"tooltip": {
"header": {
"title": "تغيير الأذونات"
}
}
}
}
}
+203 -19
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Yüklənir...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Qabaqcıl Rəng Seçimləri",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "Çevir"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Səhifələri çıxar"
},
"removePages": {
"title": "Sil",
"desc": "PDF Sənədindən istəmədiyin şəkilləri sil."
},
"removeImagePdf": {
"title": "Şəkli Sil",
"desc": "Fayl ölçüsünü azaltmaq üçün PDF-dən şəkil sil"
},
"autoSizeSplitPDF": {
"title": "Say/Ölçüyə Əsasən Avtomatik Ayır",
"desc": "PDF-i ölçüyə, səhifə sayına və ya sənəd sayına əsasən bir neçə PDF-ə ayır."
},
"adjust-contrast": {
"title": "Rəngləri/Kontrastı Tənzimlə",
"desc": "PDF-in kontrastını, parlaqlığını, rəng doyğunluğunu tənzimlə"
},
"replaceColorPdf": {
"title": "Qabaqcıl Rəng Seçimləri",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "İcazələri Dəyişdir"
}
},
"viewPdf": {
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Böl",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Sil",
"dragDropMessage": "Seçilmiş Səhifə(lər)",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Ada görə çeşidləyin",
"sortByDate": "Tarixə görə çeşidləyin",
"removeCertSign": "Birləşdirilmiş faylda rəqəmsal imza silinsin?",
"submit": "Birləşdirin"
"submit": "Birləşdirin",
"sortBy": {
"filename": "Fayl Adı"
}
},
"split": {
"tags": "Səhifə əməliyyarları,böl,Çoxlu Səhifə,kəs,server-tərəf",
@@ -634,7 +669,20 @@
"8": "Sənəd #6: Səhifə 10"
},
"splitPages": "Bölünəcək Səhifələri Daxil Edin:",
"submit": "Bölün"
"submit": "Bölün",
"steps": {
"settings": "Parametrlər"
},
"methods": {
"bySize": {
"name": "Fayl Ölçüsü"
}
},
"value": {
"fileSize": {
"label": "Fayl Ölçüsü"
}
}
},
"rotate": {
"tags": "server-tərəf",
@@ -712,6 +760,12 @@
"type": {
"1": "Mətn",
"2": "Şəkil"
},
"watermarkType": {
"text": "Yazı"
},
"settings": {
"fontSize": "Şrift Ölçüsü"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Dəyiş"
},
"removePages": {
"tags": "Səhifələri təmizlə,səhifələri sil"
"tags": "Səhifələri təmizlə,səhifələri sil",
"title": "Sil",
"submit": "Sil"
},
"addPassword": {
"tags": "təhlükəsiz,təhlükəsizlik",
@@ -758,7 +814,12 @@
"15": "Sənəd açıldıqdan sonra onunla nə edilə biləcəyini limitləndir (Bütün oxuyucular dəstəkləmir)",
"16": "Sənədin özünün açılmağını limitləndirir"
},
"submit": "Şifrlə"
"submit": "Şifrlə",
"tooltip": {
"permissions": {
"title": "İcazələri Dəyişdir"
}
}
},
"removePassword": {
"tags": "təhlükəsiz,deşifr,təhlükəsizlik,kodu aç,kodu sil",
@@ -768,7 +829,12 @@
"1": "Deşifr Üçün PDF-i Seç",
"2": "Şifr"
},
"submit": "Sil"
"submit": "Sil",
"desc": "PDF Sənədindən şifr qorumasını götür.",
"password": {
"stepTitle": "Şifri Sil",
"label": "Cari Şifr"
}
},
"compressPdfs": {
"tags": "sıx,balaca,kiçik"
@@ -829,7 +895,25 @@
},
"help": "Bunu digər dillər üçün necə istifadə etmək və/və ya docker-də istifadə etməmək üçün bu dokumentasiyanı oxuyun",
"credit": "Bu servis OST (OCR) üçün \"OCRmyPDF\" və \"Tesseract\" istifadə edir.",
"submit": "PDF-i OST ilə işlə"
"submit": "PDF-i OST ilə işlə",
"desc": "Skanları təmizləyir və PDF-in içərisindəki şəkillərdəki yazını tapıb mətn olaraq əlavə edir.",
"settings": {
"title": "Parametrlər",
"ocrMode": {
"label": "OST (OCR) Rejimi"
},
"languages": {
"label": "Dillər"
}
},
"tooltip": {
"mode": {
"title": "OST (OCR) Rejimi"
},
"languages": {
"title": "Dillər"
}
}
},
"extractImages": {
"tags": "şəkil,foto,saxla,arxiv,zip,çək,götür",
@@ -938,7 +1022,13 @@
"title": "Düzləşdirin",
"header": "PDF-i düzləşdirin",
"flattenOnlyForms": "Yalnız formaları düzəldin",
"submit": "Düzləşdirin"
"submit": "Düzləşdirin",
"steps": {
"settings": "Parametrlər"
},
"options": {
"flattenOnlyForms": "Yalnız formaları düzəldin"
}
},
"repair": {
"tags": "düzəlt,bərpa et,korreksiya et,geri qaytar",
@@ -1106,10 +1196,10 @@
"credit": "WeasyPrint İstifadə Edir"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "işarələmə,web-məzmun,dəyişmə,çevirmə",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Çevir"
},
"getPdfInfo": {
"tags": "informasiy,data,məlumatlar,statistika",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,22 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Qabaqcıl"
},
"wordsToRedact": {
"add": "Əlavə et"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Səhifələr",
"placeholder": "(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)"
},
"export": "İxrac Et"
}
},
"tableExtraxt": {
"tags": "CSV,Cədvəl xaricetmə,xaric et,çevir"
@@ -1261,8 +1366,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Səbəb",
"location": "Məkan",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1386,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Versiya",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1491,10 @@
"4": "Avto mod - PDF-in dəqiq ölçüsünü əldə etmək üçün keyfiyyəti avtomatik tənzimləyir",
"5": "Gözlənilən PDF Ölçüsü (məsələn, 25MB, 10.8MB, 25KB)"
},
"submit": "Sıxışdır"
"submit": "Sıxışdır",
"method": {
"filesize": "Fayl Ölçüsü"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1665,81 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Endir",
"convert": {
"title": "Çevir",
"settings": "Parametrlər",
"color": "Rəng",
"greyscale": "Boz Tonlama",
"fillPage": "Səhifəni Doldur",
"pdfaDigitalSignatureWarning": "PDF Rəqəmsal İmza Ehtiva Edir.Bu, növbəti addımda silinəcək.",
"grayscale": "Boz Tonlama"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Hamısını Seç",
"deselectAll": "Hamısını Seçməni Ləğv Et"
},
"quickAccess": {
"sign": "İmzala"
},
"fileUpload": {
"loading": "Yüklənir..."
},
"fileManager": {
"fileName": "Ad",
"fileVersion": "Versiya",
"selectAll": "Hamısını Seç",
"deselectAll": "Hamısını Seçməni Ləğv Et",
"deleteSelected": "Seçilmişi Sil",
"download": "Endir",
"delete": "Sil"
},
"sanitize": {
"submit": "PDF-i Təmizlə",
"steps": {
"settings": "Parametrlər"
}
},
"changePermissions": {
"title": "İcazələri Dəyişdir",
"submit": "İcazələri Dəyişdir",
"permissions": {
"preventAssembly": {
"label": "Sənədin Yığılmasının Qarşısını Al"
},
"preventExtractContent": {
"label": "Məzmunun Çıxarılmasının Qarşısını Al"
},
"preventExtractForAccessibility": {
"label": "Əlçatanlıq Üçün Çıxarılmasının Qarşısını Alın"
},
"preventFillInForm": {
"label": "Formanın Doldurulmasının Qarşısını Alır"
},
"preventModify": {
"label": "Modifikasiyanın Qarşısını Al"
},
"preventModifyAnnotations": {
"label": "Annotasiyanın Dəyişdirilməsinin Qarşısını Almaq"
},
"preventPrinting": {
"label": "Çapın Qarşısını Al"
},
"preventPrintingFaithful": {
"label": "Fərqli Formatlarda Çapın Qarşısını Al"
}
},
"tooltip": {
"header": {
"title": "İcazələri Dəyişdir"
}
}
}
}
}
+196 -12
View File
@@ -137,7 +137,7 @@
"scanHelp": "Помощ за сканиране на папки",
"deletePrompt": "Сигурни ли сте, че искате да изтриете pipeline",
"tags": "автоматизиране,последователност,чрез скриптове,пакетен процес",
"title": "Pipeline"
"title": "Pipeline (Разширено)"
},
"pipelineOptions": {
"header": "Pipeline Конфигурация",
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Зареждане на...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Замяна и обръщане на цвят",
"desc": "Заменете цвета на текста и фона в PDF и обърнете пълния цвят на PDF, за да намалите размера на файла"
},
"convert": {
"title": "Преобразуване"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Извличане на страници"
},
"removePages": {
"title": "Премахване",
"desc": "Изтрийте нежеланите страници от вашия PDF документ."
},
"removeImagePdf": {
"title": "Премахване на изображение",
"desc": "Премахнете изображението от PDF, за да намалите размера на файла"
},
"autoSizeSplitPDF": {
"title": "Автоматично разделяне по размер/брой",
"desc": "Разделете един PDF на множество документи въз основа на размер, брой страници или брой документи"
},
"adjust-contrast": {
"title": "Коригиране на цветове/контраст",
"desc": "Коригиране на контраста, наситеността и яркостта на PDF"
},
"replaceColorPdf": {
"title": "Замяна и обръщане на цвят",
"desc": "Заменете цвета на текста и фона в PDF и обърнете пълния цвят на PDF, за да намалите размера на файла"
},
"changePermissions": {
"title": "Промяна на правата"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Сортиране по име",
"sortByDate": "Сортиране по дата",
"removeCertSign": "Премахване на цифровия подпис в обединения файл?",
"submit": "Обединяване"
"submit": "Обединяване",
"sortBy": {
"filename": "Име на файл"
}
},
"split": {
"tags": "Операции на страницата,разделяне,Множество страници,изрязване,сървърна страна",
@@ -634,7 +669,20 @@
"8": "Документ #6: Страница 10"
},
"splitPages": "Въведете страници за разделяне:",
"submit": "Разделяне"
"submit": "Разделяне",
"steps": {
"settings": "Настройки"
},
"methods": {
"bySize": {
"name": "Размер на файла"
}
},
"value": {
"fileSize": {
"label": "Размер на файла"
}
}
},
"rotate": {
"tags": "от страната на сървъра",
@@ -712,6 +760,12 @@
"type": {
"1": "Текст",
"2": "Изображение"
},
"watermarkType": {
"text": "Текст"
},
"settings": {
"fontSize": "Размер на шрифт"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Промени"
},
"removePages": {
"tags": "Премахване на страници,изтриване на страници"
"tags": "Премахване на страници,изтриване на страници",
"title": "Премахване",
"submit": "Премахване"
},
"addPassword": {
"tags": "сигурен,сигурност",
@@ -758,7 +814,12 @@
"15": "Ограничава какво може да се прави с документа, след като бъде отворен (не се поддържа от всички четци)",
"16": "Ограничава отварянето на самия документ"
},
"submit": "Шифроване"
"submit": "Шифроване",
"tooltip": {
"permissions": {
"title": "Промяна на правата"
}
}
},
"removePassword": {
"tags": "сигурно,декриптиране,сигурност,отмяна на парола,изтриване на парола",
@@ -768,7 +829,12 @@
"1": "Изберете PDF за Декриптиране",
"2": "Парола"
},
"submit": "Премахване"
"submit": "Премахване",
"desc": "Премахнете защитата с парола от вашия PDF документ.",
"password": {
"stepTitle": "Премахване на парола",
"label": "Текуща парола"
}
},
"compressPdfs": {
"tags": "мачкам,малък,мъничък"
@@ -829,7 +895,25 @@
},
"help": "Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker",
"credit": "Тази услуга използва qpdf и Tesseract за OCR.",
"submit": "Обработка на PDF чрез OCR"
"submit": "Обработка на PDF чрез OCR",
"desc": "Почиства, сканира и открива текст от изображения в PDF и го добавя отново като текст.",
"settings": {
"title": "Настройки",
"ocrMode": {
"label": "OCR режим"
},
"languages": {
"label": "Езици"
}
},
"tooltip": {
"mode": {
"title": "OCR режим"
},
"languages": {
"title": "Езици"
}
}
},
"extractImages": {
"tags": "изображение,снимка,запазване,архивиране,архив,заснемане,грабване",
@@ -938,7 +1022,13 @@
"title": "Изравнете",
"header": "Изравнете PDF-и",
"flattenOnlyForms": "Изравнете само форми",
"submit": "Изравнете"
"submit": "Изравнете",
"steps": {
"settings": "Настройки"
},
"options": {
"flattenOnlyForms": "Изравнете само форми"
}
},
"repair": {
"tags": "поправка,възстановяване,корекция,възстановяване",
@@ -1174,7 +1264,21 @@
"showLayers": "Показване на слоеве (щракнете два пъти, за да върнете всички слоеве в състояние по подразбиране)",
"colourPicker": "Избор на цвят",
"findCurrentOutlineItem": "Намиране на текущия елемент от контура",
"applyChanges": "Прилагане на промените"
"applyChanges": "Прилагане на промените",
"auto": {
"settings": {
"advancedTitle": "Разширено"
},
"wordsToRedact": {
"add": "Добави"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Страници"
},
"export": "Експорт"
}
},
"tableExtraxt": {
"tags": "CSV,извличане на таблица,извличане,конвертиране"
@@ -1386,7 +1490,10 @@
"4": "Автоматичен режим - Автоматично настройва качеството, за да получи PDF с точен размер",
"5": "Очакван PDF размер (напр. 25МБ, 10.8МБ, 25КБ)"
},
"submit": "Компресиране"
"submit": "Компресиране",
"method": {
"filesize": "Размер на файла"
}
},
"decrypt": {
"passwordPrompt": "Този файл е защитен с парола. Моля, въведете паролата:",
@@ -1557,5 +1664,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Изтегли",
"convert": {
"title": "Преобразуване",
"settings": "Настройки",
"color": "Цвят",
"greyscale": "Скала на сивото",
"fillPage": "Попълване на страница",
"pdfaDigitalSignatureWarning": "PDF файлът съдържа цифров подпис. Това ще бъде премахнато в следващата стъпка.",
"grayscale": "Скала на сивото"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Изберете всички",
"deselectAll": "Отмяна на избора на всички"
},
"quickAccess": {
"sign": "Подпишете"
},
"fileUpload": {
"loading": "Зареждане на...",
"or": "или"
},
"fileManager": {
"fileName": "Име",
"fileVersion": "Версия",
"selectAll": "Изберете всички",
"deselectAll": "Отмяна на избора на всички",
"deleteSelected": "Изтриване на избраното",
"download": "Изтегли",
"delete": "Изтрий"
},
"sanitize": {
"submit": "Дезинфектирай PDF",
"steps": {
"settings": "Настройки"
}
},
"changePermissions": {
"title": "Промяна на правата",
"submit": "Промяна на правата",
"permissions": {
"preventAssembly": {
"label": "Предотвратяване на сглобяването на документ"
},
"preventExtractContent": {
"label": "Предотвратете извличането на съдържание"
},
"preventExtractForAccessibility": {
"label": "Предотвратете извличането за достъпност"
},
"preventFillInForm": {
"label": "Предотвратяване на попълване на формуляр"
},
"preventModify": {
"label": "Предотвратяване на модификация"
},
"preventModifyAnnotations": {
"label": "Предотвратяване на модификация на анотация"
},
"preventPrinting": {
"label": "Предотвратявам на отпечатването"
},
"preventPrintingFaithful": {
"label": "Предотвратете отпечатването на различни формати"
}
},
"tooltip": {
"header": {
"title": "Промяна на правата"
}
}
}
}
}
+195 -13
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Carregant...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Reemplaça i Inverteix Color",
"desc": "Reemplaça el color del text i el fons en un PDF i inverteix tot el color del PDF per reduir la mida del fitxer"
},
"convert": {
"title": "Converteix"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extreu Pàgines"
},
"removePages": {
"title": "Elimina",
"desc": "Elimina pàgines del document PDF."
},
"removeImagePdf": {
"title": "Elimina imatge",
"desc": "Elimina imatges d'un PDF per reduir la mida del fitxer"
},
"autoSizeSplitPDF": {
"title": "Divisió Automàtica per Mida/Quantitat",
"desc": "Divideix un únic PDF en múltiples documents basant-se en la mida, el nombre de pàgines o el nombre de documents"
},
"adjust-contrast": {
"title": "Ajusta Colors/Contrast",
"desc": "Ajusta colors/contrast, saturació i brillantor"
},
"replaceColorPdf": {
"title": "Reemplaça i Inverteix Color",
"desc": "Reemplaça el color del text i el fons en un PDF i inverteix tot el color del PDF per reduir la mida del fitxer"
},
"changePermissions": {
"title": "Canviar Permissos"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Ordena per nom",
"sortByDate": "Ordena per data",
"removeCertSign": "Eliminar la signatura digital en el fitxer fusionat?",
"submit": "Fusiona"
"submit": "Fusiona",
"sortBy": {
"filename": "Nom del Fitxer"
}
},
"split": {
"tags": "operacions de pàgina,divideix,Multi-Pàgina,talla,servidor",
@@ -634,7 +669,20 @@
"8": "Document #6: Pàgina 10"
},
"splitPages": "Introdueix les pàgines per dividir-les:",
"submit": "Divideix"
"submit": "Divideix",
"steps": {
"settings": "Opcions"
},
"methods": {
"bySize": {
"name": "Mida del Fitxer"
}
},
"value": {
"fileSize": {
"label": "Mida del Fitxer"
}
}
},
"rotate": {
"tags": "servidor",
@@ -712,6 +760,9 @@
"type": {
"1": "Text",
"2": "Imatge"
},
"settings": {
"fontSize": "Mida del tipus de lletra"
}
},
"permissions": {
@@ -734,7 +785,9 @@
"submit": "Canviar Permissos"
},
"removePages": {
"tags": "eliminar pàgines,suprimir pàgines"
"tags": "eliminar pàgines,suprimir pàgines",
"title": "Elimina",
"submit": "Elimina"
},
"addPassword": {
"tags": "segur,seguretat",
@@ -758,7 +811,12 @@
"15": "Restringeix el que es pot fer amb el document un cop obert (No compatible amb tots els lectors)",
"16": "Restringeix l'obertura del document"
},
"submit": "Encripta"
"submit": "Encripta",
"tooltip": {
"permissions": {
"title": "Canviar Permissos"
}
}
},
"removePassword": {
"tags": "segur,desencripta,seguretat,eliminar contrasenya,suprimir contrasenya",
@@ -768,7 +826,12 @@
"1": "Selecciona el PDF a Desxifrar",
"2": "Contrasenya"
},
"submit": "Eliminar Contrasenya"
"submit": "Eliminar Contrasenya",
"desc": "Elimina la contrasenya del document PDF.",
"password": {
"stepTitle": "Elimina Contrasenya",
"label": "Contrasenya Actual"
}
},
"compressPdfs": {
"tags": "estrènyer,petit,minúscul"
@@ -829,7 +892,25 @@
},
"help": "Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker",
"credit": "Aquest servei fa servir qpdf i Tesseract per a OCR.",
"submit": "Processa PDF amb OCR"
"submit": "Processa PDF amb OCR",
"desc": "Neteja escanejats i detecta text d'imatges dins d'un PDF, tornant-lo a afegir com a text.",
"settings": {
"title": "Opcions",
"ocrMode": {
"label": "Mode OCR"
},
"languages": {
"label": "Idiomes"
}
},
"tooltip": {
"mode": {
"title": "Mode OCR"
},
"languages": {
"title": "Idiomes"
}
}
},
"extractImages": {
"tags": "imatge,foto,desa,arxiva,zip,captura,agafa",
@@ -924,7 +1005,7 @@
"sharedSigs": "Signatures Compartides",
"noSavedSigs": "No s'han trobat signatures desades",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Esborra",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1019,13 @@
"title": "Aplanar",
"header": "Aplana els PDF",
"flattenOnlyForms": "Aplana només els formularis",
"submit": "Aplanar"
"submit": "Aplanar",
"steps": {
"settings": "Opcions"
},
"options": {
"flattenOnlyForms": "Aplana només els formularis"
}
},
"repair": {
"tags": "repara,restaura,correcció,recupera",
@@ -1109,7 +1196,7 @@
"tags": "marcatge,contingut web,transformació,convertir,md",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Converteix"
},
"getPdfInfo": {
"tags": "informació,dades,estadístiques,estadístiques",
@@ -1174,7 +1261,22 @@
"showLayers": "Mostra les capes (doble clic per restablir totes les capes a l'estat per defecte)",
"colourPicker": "Selector de colors",
"findCurrentOutlineItem": "Troba l'element actual de l'esquema",
"applyChanges": "Aplica els canvis"
"applyChanges": "Aplica els canvis",
"auto": {
"settings": {
"advancedTitle": "Avançat"
},
"wordsToRedact": {
"add": "Afegeix"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Pàgines",
"placeholder": "(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)"
},
"export": "Exporta"
}
},
"tableExtraxt": {
"tags": "CSV,Extracció de taules,extreure,convertir"
@@ -1386,7 +1488,10 @@
"4": "Mode automàtic: ajusta automàticament la qualitat perquè el PDF tingui la mida exacta",
"5": "Mida esperada del PDF (p. ex. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Comprimir"
"submit": "Comprimir",
"method": {
"filesize": "Mida del Fitxer"
}
},
"decrypt": {
"passwordPrompt": "Aquest fitxer està protegit amb contrasenya. Si us plau, introdueix la contrasenya:",
@@ -1557,5 +1662,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Descarrega",
"convert": {
"title": "Converteix",
"settings": "Opcions",
"color": "Color",
"greyscale": "Escala de Grisos",
"fillPage": "Omple la Pàgina",
"pdfaDigitalSignatureWarning": "El PDF conté una signatura digital. Aquesta serà eliminada en el següent pas.",
"grayscale": "Escala de Grisos"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Selecciona-ho tot",
"deselectAll": "Desselecciona-ho tot"
},
"quickAccess": {
"sign": "Signa"
},
"fileUpload": {
"loading": "Carregant...",
"or": "o"
},
"fileManager": {
"fileName": "Nom",
"fileVersion": "Versió",
"selectAll": "Selecciona-ho tot",
"deselectAll": "Desselecciona-ho tot",
"deleteSelected": "Suprimeix seleccionades",
"download": "Descarrega",
"delete": "Esborra"
},
"sanitize": {
"submit": "Neteja PDF",
"steps": {
"settings": "Opcions"
}
},
"changePermissions": {
"title": "Canviar Permissos",
"submit": "Canviar Permissos",
"permissions": {
"preventAssembly": {
"label": "Evita el muntatge del document"
},
"preventExtractContent": {
"label": "Evita l'extracció de contingut"
},
"preventExtractForAccessibility": {
"label": "Evita l'extracció de contingut per accessibilitat"
},
"preventFillInForm": {
"label": "Evita emplenar formularis"
},
"preventModify": {
"label": "Evita modificacions"
},
"preventModifyAnnotations": {
"label": "Evita modificacions d'annotacions"
},
"preventPrinting": {
"label": "Evita impressió"
},
"preventPrintingFaithful": {
"label": "Evita impressió en diferents formats"
}
},
"tooltip": {
"header": {
"title": "Canviar Permissos"
}
}
}
}
}
+193 -11
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Načítání...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Nahrazení a inverze barev",
"desc": "Úprava barev textu a pozadí v PDF nebo jejich inverze ke snížení velikosti souboru"
},
"convert": {
"title": "Převést"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrahovat stránky"
},
"removePages": {
"title": "Odstranit",
"desc": "Smazat nežádoucí stránky z vašeho PDF dokumentu."
},
"removeImagePdf": {
"title": "Odstranit obrázek",
"desc": "Odstranit obrázek z PDF pro zmenšení velikosti souboru"
},
"autoSizeSplitPDF": {
"title": "Automaticky rozdělit podle velikosti/počtu",
"desc": "Rozdělí jeden PDF na více dokumentů podle velikosti, počtu stránek nebo počtu dokumentů"
},
"adjust-contrast": {
"title": "Upravit barvy/kontrast",
"desc": "Upravit kontrast, sytost a jas PDF"
},
"replaceColorPdf": {
"title": "Nahrazení a inverze barev",
"desc": "Úprava barev textu a pozadí v PDF nebo jejich inverze ke snížení velikosti souboru"
},
"changePermissions": {
"title": "Změnit oprávnění"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Seřadit podle názvu",
"sortByDate": "Seřadit podle data",
"removeCertSign": "Odstranit digitální podpis v sloučeném souboru?",
"submit": "Sloučit"
"submit": "Sloučit",
"sortBy": {
"filename": "Název souboru"
}
},
"split": {
"tags": "Operace se stránkami,rozdělit,Více stránek,vyjmout,serverová strana",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Stránka 10"
},
"splitPages": "Zadejte stránky pro rozdělení:",
"submit": "Rozdělit"
"submit": "Rozdělit",
"steps": {
"settings": "Nastavení"
},
"methods": {
"bySize": {
"name": "Velikost souboru"
}
},
"value": {
"fileSize": {
"label": "Velikost souboru"
}
}
},
"rotate": {
"tags": "serverová strana",
@@ -712,6 +760,9 @@
"type": {
"1": "Text",
"2": "Obrázek"
},
"settings": {
"fontSize": "Velikost písma"
}
},
"permissions": {
@@ -734,7 +785,9 @@
"submit": "Změnit"
},
"removePages": {
"tags": "Odstranit stránky,smazat stránky"
"tags": "Odstranit stránky,smazat stránky",
"title": "Odstranit",
"submit": "Odstranit"
},
"addPassword": {
"tags": "zabezpečit,bezpečnost",
@@ -758,7 +811,12 @@
"15": "Omezuje, co lze s dokumentem dělat po jeho otevření (Není podporováno všemi čtečkami)",
"16": "Omezuje samotné otevření dokumentu"
},
"submit": "Zašifrovat"
"submit": "Zašifrovat",
"tooltip": {
"permissions": {
"title": "Změnit oprávnění"
}
}
},
"removePassword": {
"tags": "zabezpečit,dešifrovat,bezpečnost,odstranit heslo,smazat heslo",
@@ -768,7 +826,12 @@
"1": "Vyberte PDF k dešifrování",
"2": "Heslo"
},
"submit": "Odstranit"
"submit": "Odstranit",
"desc": "Odstranit ochranu heslem z vašeho PDF dokumentu.",
"password": {
"stepTitle": "Odstranit heslo",
"label": "Současné heslo"
}
},
"compressPdfs": {
"tags": "stlačit,malý,drobný"
@@ -829,7 +892,25 @@
},
"help": "Přečtěte si prosím tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker",
"credit": "Tato služba používá qpdf a Tesseract pro OCR.",
"submit": "Zpracovat PDF pomocí OCR"
"submit": "Zpracovat PDF pomocí OCR",
"desc": "Vyčistí skeny a detekuje text z obrázků v PDF a znovu ho přidá jako text.",
"settings": {
"title": "Nastavení",
"ocrMode": {
"label": "Režim OCR"
},
"languages": {
"label": "Jazyky"
}
},
"tooltip": {
"mode": {
"title": "Režim OCR"
},
"languages": {
"title": "Jazyky"
}
}
},
"extractImages": {
"tags": "obrázek,fotka,uložit,archiv,zip,zachytit,získat",
@@ -938,7 +1019,13 @@
"title": "Zploštit",
"header": "Zploštit PDF",
"flattenOnlyForms": "Zploštit pouze formuláře",
"submit": "Zploštit"
"submit": "Zploštit",
"steps": {
"settings": "Nastavení"
},
"options": {
"flattenOnlyForms": "Zploštit pouze formuláře"
}
},
"repair": {
"tags": "opravit,obnovit,korekce,obnovit",
@@ -1174,7 +1261,22 @@
"showLayers": "Zobrazit vrstvy (dvojklik pro obnovení všech vrstev do výchozího stavu)",
"colourPicker": "Výběr barvy",
"findCurrentOutlineItem": "Najít aktuální položku osnovy",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Pokročilé"
},
"wordsToRedact": {
"add": "Přidat"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Stránky",
"placeholder": "(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)"
},
"export": "Exportovat"
}
},
"tableExtraxt": {
"tags": "CSV,Extrakce tabulek,extrahovat,převést"
@@ -1386,7 +1488,10 @@
"4": "Automatický režim - Automaticky upravuje kvalitu pro dosažení přesné velikosti PDF",
"5": "Očekávaná velikost PDF (např. 25MB, 10.8MB, 25KB)"
},
"submit": "Komprimovat"
"submit": "Komprimovat",
"method": {
"filesize": "Velikost souboru"
}
},
"decrypt": {
"passwordPrompt": "Tento soubor je chráněn heslem. Zadejte prosím heslo:",
@@ -1557,5 +1662,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Stáhnout",
"convert": {
"title": "Převést",
"settings": "Nastavení",
"color": "Barva",
"greyscale": "Stupně šedi",
"fillPage": "Vyplnit stránku",
"pdfaDigitalSignatureWarning": "PDF obsahuje digitální podpis, který bude v dalším kroku odstraněn.",
"grayscale": "Stupně šedi"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Vybrat vše",
"deselectAll": "Zrušit výběr všeho"
},
"quickAccess": {
"sign": "Podepsat"
},
"fileUpload": {
"loading": "Načítání...",
"or": "nebo"
},
"fileManager": {
"fileName": "Jméno",
"fileVersion": "Verze",
"selectAll": "Vybrat vše",
"deselectAll": "Zrušit výběr všeho",
"deleteSelected": "Smazat vybrané",
"download": "Stáhnout",
"delete": "Smazat"
},
"sanitize": {
"submit": "Sanitizovat PDF",
"steps": {
"settings": "Nastavení"
}
},
"changePermissions": {
"title": "Změnit oprávnění",
"submit": "Změnit oprávnění",
"permissions": {
"preventAssembly": {
"label": "Zabránit sestavení dokumentu"
},
"preventExtractContent": {
"label": "Zabránit extrakci obsahu"
},
"preventExtractForAccessibility": {
"label": "Zabránit extrakci pro přístupnost"
},
"preventFillInForm": {
"label": "Zabránit vyplňování formulářů"
},
"preventModify": {
"label": "Zabránit úpravám"
},
"preventModifyAnnotations": {
"label": "Zabránit úpravám poznámek"
},
"preventPrinting": {
"label": "Zabránit tisku"
},
"preventPrintingFaithful": {
"label": "Zabránit tisku v různých formátech"
}
},
"tooltip": {
"header": {
"title": "Změnit oprávnění"
}
}
}
}
}
+194 -21
View File
@@ -137,7 +137,7 @@
"scanHelp": "Mappe Scanning Hjælp",
"deletePrompt": "Er du sikker på, at du vil slette pipeline",
"tags": "automatisér,sekvens,scriptet,batch-proces",
"title": "Pipeline"
"title": "Pipeline (Avanceret)"
},
"pipelineOptions": {
"header": "Pipeline Konfiguration",
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Laster...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Erstatt farve for tekst og baggrund i en PDF og omgivende farve til fuld farve af PDF for at redusere filstørrelsen."
},
"convert": {
"title": "Konvertér"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Udtræk Sider"
},
"removePages": {
"title": "Fjern",
"desc": "Slet uønskede sider fra dit PDF-dokument."
},
"removeImagePdf": {
"title": "Fjern billede",
"desc": "Fjern billede fra PDF for at reducere filstørrelse"
},
"autoSizeSplitPDF": {
"title": "Auto Opdel efter Størrelse/Antal",
"desc": "Opdel en enkelt PDF i flere dokumenter baseret på størrelse, sideantal eller dokumentantal"
},
"adjust-contrast": {
"title": "Justér Farver/Kontrast",
"desc": "Justér Kontrast, Mætning og Lysstyrke af en PDF"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Erstatt farve for tekst og baggrund i en PDF og omgivende farve til fuld farve af PDF for at redusere filstørrelsen."
},
"changePermissions": {
"title": "Ændre Tilladelser"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "Sidenummer",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Opdel",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Slet",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sortér efter navn",
"sortByDate": "Sortér efter dato",
"removeCertSign": "Fjern digital signatur i den flettede fil?",
"submit": "Flet"
"submit": "Flet",
"sortBy": {
"filename": "Filnavn"
}
},
"split": {
"tags": "Sideoperationer,opdel,Multi Side,klip,server side",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Side 10"
},
"splitPages": "Indtast sider at opdele på:",
"submit": "Opdel"
"submit": "Opdel",
"steps": {
"settings": "Indstillinger"
},
"methods": {
"bySize": {
"name": "Filstørrelse"
}
},
"value": {
"fileSize": {
"label": "Filstørrelse"
}
}
},
"rotate": {
"tags": "server side",
@@ -712,6 +760,12 @@
"type": {
"1": "Tekst",
"2": "Billede"
},
"watermarkType": {
"text": "Tekst"
},
"settings": {
"fontSize": "Skriftstørrelse"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Ændre"
},
"removePages": {
"tags": "Fjern sider,slet sider"
"tags": "Fjern sider,slet sider",
"title": "Fjern",
"submit": "Fjern"
},
"addPassword": {
"tags": "sikker,sikkerhed",
@@ -758,7 +814,12 @@
"15": "Begrænser hvad der kan gøres med dokumentet, når det er åbnet (Understøttes ikke af alle læsere)",
"16": "Begrænser åbningen af selve dokumentet"
},
"submit": "Kryptér"
"submit": "Kryptér",
"tooltip": {
"permissions": {
"title": "Ændre Tilladelser"
}
}
},
"removePassword": {
"tags": "sikker,Dekryptér,sikkerhed,fjern adgangskode,slet adgangskode",
@@ -768,7 +829,12 @@
"1": "Vælg PDF til Dekryptering",
"2": "Adgangskode"
},
"submit": "Fjern"
"submit": "Fjern",
"desc": "Fjern adgangskodebeskyttelse fra dit PDF-dokument.",
"password": {
"stepTitle": "Fjern Adgangskode",
"label": "Nuværende Adgangskode"
}
},
"compressPdfs": {
"tags": "klem,lille,tiny"
@@ -829,7 +895,25 @@
},
"help": "Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker",
"credit": "Denne tjeneste bruger qpdf og Tesseract til OCR.",
"submit": "Behandl PDF med OCR"
"submit": "Behandl PDF med OCR",
"desc": "Oprydning af scanninger og genkender tekst fra billeder i en PDF og tilføjer den igen som tekst.",
"settings": {
"title": "Indstillinger",
"ocrMode": {
"label": "OCR-tilstand"
},
"languages": {
"label": "Sprog"
}
},
"tooltip": {
"mode": {
"title": "OCR-tilstand"
},
"languages": {
"title": "Sprog"
}
}
},
"extractImages": {
"tags": "billede,foto,gem,arkiv,zip,fang,grib",
@@ -924,7 +1008,7 @@
"sharedSigs": "Delte Signaturer",
"noSavedSigs": "Ingen Gemte Signaturer Fundet",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Slet",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "Udjævn",
"header": "Udjævn PDF",
"flattenOnlyForms": "Udjævn kun formularer",
"submit": "Udjævn"
"submit": "Udjævn",
"steps": {
"settings": "Indstillinger"
},
"options": {
"flattenOnlyForms": "Udjævn kun formularer"
}
},
"repair": {
"tags": "fix,gendan,korrektion,genvind",
@@ -1106,10 +1196,10 @@
"credit": "Bruger WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,webindhold,transformation,konvertér",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Konvertér"
},
"getPdfInfo": {
"tags": "information,data,statistik,statistikker",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,21 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Avanceret"
},
"wordsToRedact": {
"add": "Tilføj"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Sideantal",
"placeholder": "(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,Tabeludtrækning,udtræk,konvertér"
@@ -1261,8 +1365,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Årsag",
"location": "Placering",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1386,7 +1490,10 @@
"4": "Auto tilstand - Justerer automatisk kvaliteten for at få PDF'en til en præcis størrelse",
"5": "Forventet PDF-størrelse (f.eks. 25MB, 10.8MB, 25KB)"
},
"submit": "Komprimer"
"submit": "Komprimer",
"method": {
"filesize": "Filstørrelse"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1664,71 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"convert": {
"title": "Konvertér",
"settings": "Indstillinger",
"color": "Farve",
"greyscale": "Gråtone",
"fillPage": "Udfyld Side",
"pdfaDigitalSignatureWarning": "PDF'en indeholder en digital signatur. Dette vil blive fjernet i næste trin.",
"grayscale": "Gråtone"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Underskriv"
},
"fileUpload": {
"loading": "Laster..."
},
"fileManager": {
"fileName": "Navn",
"delete": "Slet"
},
"sanitize": {
"submit": "Rens PDF",
"steps": {
"settings": "Indstillinger"
}
},
"changePermissions": {
"title": "Ændre Tilladelser",
"submit": "Ændre Tilladelser",
"permissions": {
"preventAssembly": {
"label": "Forhindre samling af dokument"
},
"preventExtractContent": {
"label": "Forhindre indholdsudtrækning"
},
"preventExtractForAccessibility": {
"label": "Forhindre udtrækning for tilgængelighed"
},
"preventFillInForm": {
"label": "Forhindre udfyldning af formular"
},
"preventModify": {
"label": "Forhindre ændring"
},
"preventModifyAnnotations": {
"label": "Forhindre anmærkningsændring"
},
"preventPrinting": {
"label": "Forhindre udskrivning"
},
"preventPrintingFaithful": {
"label": "Forhindre udskrivning af forskellige formater"
}
},
"tooltip": {
"header": {
"title": "Ændre Tilladelser"
}
}
}
}
}
+201 -15
View File
@@ -388,8 +388,8 @@
"desc": "PDF komprimieren um die Dateigröße zu reduzieren"
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "Schreibgeschützte PDF-Formfelder entfernen",
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument."
},
"changeMetadata": {
"title": "Metadaten ändern",
@@ -578,6 +578,43 @@
"replace-color": {
"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"
},
"convert": {
"title": "Umwandeln"
},
"attachments": {
"title": "Anhänge hinzufügen",
"desc": "Eingebettete Dateien (Anhänge) zu einem PDF hinzufügen oder daraus entfernen"
},
"editTableOfContents": {
"title": "Inhaltsverzeichnis bearbeiten",
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten"
},
"extractPages": {
"title": "Seiten extrahieren"
},
"removePages": {
"title": "Entfernen",
"desc": "Ungewollte Seiten aus dem PDF entfernen"
},
"removeImagePdf": {
"title": "Bild entfernen",
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern"
},
"autoSizeSplitPDF": {
"title": "Teilen nach Größe/Anzahl",
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf"
},
"adjust-contrast": {
"title": "Farben/Kontrast anpassen",
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen"
},
"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"
},
"changePermissions": {
"title": "Berechtigungen ändern"
}
},
"viewPdf": {
@@ -617,7 +654,10 @@
"sortByName": "Nach Namen sortieren",
"sortByDate": "Nach Datum sortieren",
"removeCertSign": "Digitale Signatur in der zusammengeführten Datei entfernen?",
"submit": "Zusammenführen"
"submit": "Zusammenführen",
"sortBy": {
"filename": "Dateiname"
}
},
"split": {
"tags": "seitenoperationen,teilen,mehrseitig,ausschneiden,serverseitig",
@@ -634,7 +674,20 @@
"8": "Dokument #6: Seite 10"
},
"splitPages": "Geben Sie die Seiten an, an denen aufgeteilt werden soll:",
"submit": "Aufteilen"
"submit": "Aufteilen",
"steps": {
"settings": "Einstellungen"
},
"methods": {
"bySize": {
"name": "Dateigröße"
}
},
"value": {
"fileSize": {
"label": "Dateigröße"
}
}
},
"rotate": {
"tags": "serverseitig",
@@ -712,6 +765,9 @@
"type": {
"1": "Text",
"2": "Bild"
},
"settings": {
"fontSize": "Schriftgröße"
}
},
"permissions": {
@@ -734,7 +790,9 @@
"submit": "Ändern"
},
"removePages": {
"tags": "seiten entfernen,seiten löschen"
"tags": "seiten entfernen,seiten löschen",
"title": "Entfernen",
"submit": "Entfernen"
},
"addPassword": {
"tags": "sicher,sicherheit",
@@ -758,7 +816,12 @@
"15": "Schränkt ein, was mit dem Dokument gemacht werden kann, sobald es geöffnet ist (wird nicht von allen Leseprogrammen unterstützt)",
"16": "Schränkt das Öffnen des Dokuments selbst ein"
},
"submit": "Verschlüsseln"
"submit": "Verschlüsseln",
"tooltip": {
"permissions": {
"title": "Berechtigungen ändern"
}
}
},
"removePassword": {
"tags": "sichern,entschlüsseln,sicherheit,passwort aufheben,passwort löschen",
@@ -768,15 +831,20 @@
"1": "Das zu entschlüsselnde PDF auswählen",
"2": "Passwort"
},
"submit": "Entfernen"
"submit": "Entfernen",
"desc": "Den Passwortschutz eines PDFs entfernen",
"password": {
"stepTitle": "Passwort entfernen",
"label": "Aktuelles Passwort"
}
},
"compressPdfs": {
"tags": "komprimieren,verkleinern,minimieren"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"tags": "entfernen,löschen,form,feld,schreibgeschützt",
"title": "Entfernen Sie schreibgeschützte Formfelder",
"header": "Schreibgeschützte PDF-Formfelder entfernen",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +897,25 @@
},
"help": "Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können",
"credit": "Dieser Dienst verwendet qpdf und Tesseract für OCR.",
"submit": "PDF mit OCR verarbeiten"
"submit": "PDF mit OCR verarbeiten",
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu",
"settings": {
"title": "Einstellungen",
"ocrMode": {
"label": "OCR-Modus"
},
"languages": {
"label": "Sprachen"
}
},
"tooltip": {
"mode": {
"title": "OCR-Modus"
},
"languages": {
"title": "Sprachen"
}
}
},
"extractImages": {
"tags": "bild,foto,speichern,archivieren,zippen,erfassen,greifen",
@@ -938,7 +1024,13 @@
"title": "Abflachen",
"header": "PDFs reduzieren",
"flattenOnlyForms": "Nur Formulare abflachen",
"submit": "Abflachen"
"submit": "Abflachen",
"steps": {
"settings": "Einstellungen"
},
"options": {
"flattenOnlyForms": "Nur Formulare abflachen"
}
},
"repair": {
"tags": "reparieren,wiederherstellen,korrigieren,wiederherstellen",
@@ -1174,7 +1266,22 @@
"showLayers": "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzustand zurückzusetzen)",
"colourPicker": "Farbauswahl",
"findCurrentOutlineItem": "Aktuell gewähltes Element finden",
"applyChanges": "Änderungen übernehmen"
"applyChanges": "Änderungen übernehmen",
"auto": {
"settings": {
"advancedTitle": "Erweiterte Funktionen"
},
"wordsToRedact": {
"add": "Signieren"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Seiten",
"placeholder": "(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1)"
},
"export": "Downloaden"
}
},
"tableExtraxt": {
"tags": "CSV,tabelle,extrahieren"
@@ -1386,7 +1493,10 @@
"4": "Automatischer Modus – Passt die Qualität automatisch an, um das PDF auf die exakte Größe zu bringen",
"5": "Erwartete PDF-Größe (z.B. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Komprimieren"
"submit": "Komprimieren",
"method": {
"filesize": "Dateigröße"
}
},
"decrypt": {
"passwordPrompt": "Diese Datei ist passwortgeschützt. Bitte geben Sie das Passwort ein:",
@@ -1557,5 +1667,81 @@
"description": "Diese Cookies helfen uns zu verstehen, wie unsere Tools genutzt werden, damit wir uns darauf konzentrieren können, die Funktionen zu entwickeln, die unserer Community am meisten am Herzen liegen. Seien Sie beruhigt – Stirling PDF kann und wird niemals den Inhalt der Dokumente verfolgen, mit denen Sie arbeiten."
}
}
},
"download": "Herunterladen",
"undo": "Rückgängig",
"convert": {
"title": "Umwandeln",
"settings": "Einstellungen",
"color": "Farbe",
"greyscale": "Graustufen",
"fillPage": "Seite füllen",
"pdfaDigitalSignatureWarning": "Das PDF enthält eine digitale Signatur. Sie wird im nächsten Schritt entfernt.",
"grayscale": "Graustufen"
},
"attachments": {
"tags": "einbetten, anhängen, datei, anhang, anhänge",
"title": "Anhänge hinzufügen",
"header": "Anhänge hinzufügen",
"submit": "Anhänge hinzufügen"
},
"rightRail": {
"selectAll": "Alle auswählen",
"deselectAll": "Auswahl aufheben"
},
"quickAccess": {
"sign": "Signieren"
},
"fileUpload": {
"loading": "Laden...",
"or": "oder"
},
"fileManager": {
"selectAll": "Alle auswählen",
"deselectAll": "Auswahl aufheben",
"deleteSelected": "Auswahl löschen",
"download": "Herunterladen",
"delete": "Löschen"
},
"sanitize": {
"submit": "PDF Bereinigen",
"steps": {
"settings": "Einstellungen"
}
},
"changePermissions": {
"title": "Berechtigungen ändern",
"submit": "Berechtigungen ändern",
"permissions": {
"preventAssembly": {
"label": "Das zusammensetzen des PDFs verhindern"
},
"preventExtractContent": {
"label": "Inhaltsextrahierung verhindern"
},
"preventExtractForAccessibility": {
"label": "Inhaltsextrahierung zur Barrierefreiheit verhindern"
},
"preventFillInForm": {
"label": "Ausfüllen des Formulars verhindern"
},
"preventModify": {
"label": "Modifizierung verhindern"
},
"preventModifyAnnotations": {
"label": "Ändern von Kommentaren verhindern"
},
"preventPrinting": {
"label": "Drucken verhindern"
},
"preventPrintingFaithful": {
"label": "Drucken verschiedener Formate verhindern"
}
},
"tooltip": {
"header": {
"title": "Berechtigungen ändern"
}
}
}
}
}
+198 -13
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Φόρτωση...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Αντικατάσταση και αναστροφή χρώματος",
"desc": "Αντικατάσταση χρώματος για κείμενο και φόντο σε PDF και αναστροφή πλήρους χρώματος για μείωση μεγέθους αρχείου"
},
"convert": {
"title": "Μετατροπή"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Εξαγωγή σελίδων"
},
"removePages": {
"title": "Αφαίρεση",
"desc": "Διαγραφή ανεπιθύμητων σελίδων από το έγγραφο PDF."
},
"removeImagePdf": {
"title": "Αφαίρεση εικόνας",
"desc": "Αφαίρεση εικόνας από PDF για μείωση μεγέθους αρχείου"
},
"autoSizeSplitPDF": {
"title": "Αυτόματος διαχωρισμός ανά μέγεθος/πλήθος",
"desc": "Διαχωρισμός ενός PDF σε πολλαπλά έγγραφα βάσει μεγέθους, αριθμού σελίδων ή αριθμού εγγράφων"
},
"adjust-contrast": {
"title": "Προσαρμογή χρωμάτων/αντίθεσης",
"desc": "Προσαρμογή αντίθεσης, κορεσμού και φωτεινότητας ενός PDF"
},
"replaceColorPdf": {
"title": "Αντικατάσταση και αναστροφή χρώματος",
"desc": "Αντικατάσταση χρώματος για κείμενο και φόντο σε PDF και αναστροφή πλήρους χρώματος για μείωση μεγέθους αρχείου"
},
"changePermissions": {
"title": "Αλλαγή δικαιωμάτων"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Ταξινόμηση κατά όνομα",
"sortByDate": "Ταξινόμηση κατά ημερομηνία",
"removeCertSign": "Αφαίρεση ψηφιακής υπογραφής στο συγχωνευμένο αρχείο;",
"submit": "Συγχώνευση"
"submit": "Συγχώνευση",
"sortBy": {
"filename": "Όνομα αρχείου"
}
},
"split": {
"tags": "λειτουργίες σελίδων,διαίρεση,πολλαπλές σελίδες,κοπή,πλευρά διακομιστή",
@@ -634,7 +669,20 @@
"8": "Έγγραφο #6: Σελίδα 10"
},
"splitPages": "Εισάγετε σελίδες για διαχωρισμό:",
"submit": "Διαχωρισμός"
"submit": "Διαχωρισμός",
"steps": {
"settings": "Ρυθμίσεις"
},
"methods": {
"bySize": {
"name": "Μέγεθος αρχείου"
}
},
"value": {
"fileSize": {
"label": "Μέγεθος αρχείου"
}
}
},
"rotate": {
"tags": "πλευρά διακομιστή",
@@ -712,6 +760,12 @@
"type": {
"1": "Κείμενο",
"2": "Εικόνα"
},
"watermarkType": {
"text": "Κείμενο"
},
"settings": {
"fontSize": "Μέγεθος γραμματοσειράς"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Αλλαγή"
},
"removePages": {
"tags": "αφαίρεση σελίδων,διαγραφή σελίδων"
"tags": "αφαίρεση σελίδων,διαγραφή σελίδων",
"title": "Αφαίρεση",
"submit": "Αφαίρεση"
},
"addPassword": {
"tags": "ασφάλεια,ασφάλεια",
@@ -758,7 +814,12 @@
"15": "Περιορίζει τι μπορεί να γίνει με το έγγραφο μετά το άνοιγμά του (Δεν υποστηρίζεται από όλα τα προγράμματα ανάγνωσης)",
"16": "Περιορίζει το άνοιγμα του ίδιου του εγγράφου"
},
"submit": "Κρυπτογράφηση"
"submit": "Κρυπτογράφηση",
"tooltip": {
"permissions": {
"title": "Αλλαγή δικαιωμάτων"
}
}
},
"removePassword": {
"tags": "ασφάλεια,αποκρυπτογράφηση,ασφάλεια,αφαίρεση κωδικού,διαγραφή κωδικού",
@@ -768,7 +829,12 @@
"1": "Επιλέξτε PDF για αποκρυπτογράφηση",
"2": "Κωδικός"
},
"submit": "Αφαίρεση"
"submit": "Αφαίρεση",
"desc": "Αφαίρεση προστασίας κωδικού από το έγγραφο PDF.",
"password": {
"stepTitle": "Αφαίρεση κωδικού",
"label": "Τρέχων κωδικός"
}
},
"compressPdfs": {
"tags": "συμπίεση,μικρό,μικροσκοπικό"
@@ -829,7 +895,25 @@
},
"help": "Παρακαλώ διαβάστε αυτή την τεκμηρίωση για το πώς να το χρησιμοποιήσετε για άλλες γλώσσες ή/και χρήση εκτός docker",
"credit": "Αυτή η υπηρεσία χρησιμοποιεί qpdf και Tesseract για OCR.",
"submit": "Επεξεργασία PDF με OCR"
"submit": "Επεξεργασία PDF με OCR",
"desc": "Καθαρισμός σαρώσεων και ανίχνευση κειμένου από εικόνες μέσα σε PDF και επαναπροσθήκη ως κείμενο.",
"settings": {
"title": "Ρυθμίσεις",
"ocrMode": {
"label": "Λειτουργία OCR"
},
"languages": {
"label": "Γλώσσες"
}
},
"tooltip": {
"mode": {
"title": "Λειτουργία OCR"
},
"languages": {
"title": "Γλώσσες"
}
}
},
"extractImages": {
"tags": "εικόνα,φωτογραφία,αποθήκευση,αρχείο,zip,σύλληψη,λήψη",
@@ -938,7 +1022,13 @@
"title": "Ισοπέδωση",
"header": "Ισοπέδωση PDF",
"flattenOnlyForms": "Ισοπέδωση μόνο φορμών",
"submit": "Ισοπέδωση"
"submit": "Ισοπέδωση",
"steps": {
"settings": "Ρυθμίσεις"
},
"options": {
"flattenOnlyForms": "Ισοπέδωση μόνο φορμών"
}
},
"repair": {
"tags": "επιδιόρθωση,αποκατάσταση,διόρθωση,ανάκτηση",
@@ -1106,10 +1196,10 @@
"credit": "Χρησιμοποιεί το WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,περιεχόμενο-web,μετατροπή,μετατροπή",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Μετατροπή"
},
"getPdfInfo": {
"tags": "πληροφορίες,δεδομένα,στατιστικά,στατιστική",
@@ -1174,7 +1264,22 @@
"showLayers": "Εμφάνιση επιπέδων (διπλό κλικ για επαναφορά όλων των επιπέδων στην προεπιλεγμένη κατάσταση)",
"colourPicker": "Επιλογέας χρώματος",
"findCurrentOutlineItem": "Εύρεση τρέχοντος στοιχείου περιγράμματος",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Προχωρημένα"
},
"wordsToRedact": {
"add": "Προσθήκη"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Σελίδες",
"placeholder": "(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)"
},
"export": "Εξαγωγή"
}
},
"tableExtraxt": {
"tags": "CSV,εξαγωγή πίνακα,εξαγωγή,μετατροπή"
@@ -1386,7 +1491,10 @@
"4": "Αυτόματη λειτουργία - Αυτόματη προσαρμογή ποιότητας για επίτευξη ακριβούς μεγέθους PDF",
"5": "Αναμενόμενο μέγεθος PDF (π.χ. 25MB, 10.8MB, 25KB)"
},
"submit": "Συμπίεση"
"submit": "Συμπίεση",
"method": {
"filesize": "Μέγεθος αρχείου"
}
},
"decrypt": {
"passwordPrompt": "Αυτό το αρχείο προστατεύεται με κωδικό πρόσβασης. Παρακαλώ εισάγετε τον κωδικό:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Λήψη",
"convert": {
"title": "Μετατροπή",
"settings": "Ρυθμίσεις",
"color": "Χρώμα",
"greyscale": "Κλίμακα του γκρι",
"fillPage": "Γέμισμα σελίδας",
"pdfaDigitalSignatureWarning": "Το PDF περιέχει ψηφιακή υπογραφή. Αυτή θα αφαιρεθεί στο επόμενο βήμα.",
"grayscale": "Κλίμακα του γκρι"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Επιλογή όλων",
"deselectAll": "Αποεπιλογή όλων"
},
"quickAccess": {
"sign": "Υπογραφή"
},
"fileUpload": {
"loading": "Φόρτωση...",
"or": "ή"
},
"fileManager": {
"fileName": "Όνομα",
"fileVersion": "Έκδοση",
"selectAll": "Επιλογή όλων",
"deselectAll": "Αποεπιλογή όλων",
"deleteSelected": "Διαγραφή επιλεγμένων",
"download": "Λήψη",
"delete": "Διαγραφή"
},
"sanitize": {
"submit": "Εξυγίανση PDF",
"steps": {
"settings": "Ρυθμίσεις"
}
},
"changePermissions": {
"title": "Αλλαγή δικαιωμάτων",
"submit": "Αλλαγή δικαιωμάτων",
"permissions": {
"preventAssembly": {
"label": "Αποτροπή συναρμολόγησης εγγράφου"
},
"preventExtractContent": {
"label": "Αποτροπή εξαγωγής περιεχομένου"
},
"preventExtractForAccessibility": {
"label": "Αποτροπή εξαγωγής για προσβασιμότητα"
},
"preventFillInForm": {
"label": "Αποτροπή συμπλήρωσης φόρμας"
},
"preventModify": {
"label": "Αποτροπή τροποποίησης"
},
"preventModifyAnnotations": {
"label": "Αποτροπή τροποποίησης σχολιασμών"
},
"preventPrinting": {
"label": "Αποτροπή εκτύπωσης"
},
"preventPrintingFaithful": {
"label": "Αποτροπή εκτύπωσης σε διαφορετικές μορφές"
}
},
"tooltip": {
"header": {
"title": "Αλλαγή δικαιωμάτων"
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+156 -5
View File
@@ -55,6 +55,7 @@
"bored": "Bored Waiting?",
"alphabet": "Alphabet",
"downloadPdf": "Download PDF",
"text": "Text",
"font": "Font",
"selectFillter": "-- Select --",
@@ -607,6 +608,10 @@
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
}
},
"landing": {
"addFiles": "Add Files",
"uploadFromComputer": "Upload from computer"
},
"viewPdf": {
"tags": "view,read,annotate,text,image,highlight,edit",
"title": "View/Edit PDF",
@@ -738,7 +743,72 @@
"submit": "Change"
},
"removePages": {
"tags": "Remove pages,delete pages"
"tags": "Remove pages,delete pages",
"title": "Remove Pages",
"pageNumbers": "Pages to Remove",
"pageNumbersPlaceholder": "e.g. 1,3,5-7",
"pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7",
"filenamePrefix": "pages_removed",
"files": {
"placeholder": "Select a PDF file in the main view to get started"
},
"settings": {
"title": "Page Selection"
},
"error": {
"failed": "An error occurred while removing pages."
},
"results": {
"title": "Page Removal Results"
},
"submit": "Remove Pages"
},
"pageSelection": {
"tooltip": {
"header": {
"title": "Page Selection Guide"
},
"basic": {
"title": "Basic Usage",
"text": "Select specific pages from your PDF document using simple syntax.",
"bullet1": "Individual pages: 1,3,5",
"bullet2": "Page ranges: 3-6 or 10-15",
"bullet3": "All pages: all"
},
"advanced": {
"title": "Advanced Features",
"expandText": "▶ Show advanced options",
"collapseText": "▼ Hide advanced options",
"mathematical": {
"title": "Mathematical Functions",
"text": "Use mathematical expressions to select page patterns:",
"bullet1": "2n - all even pages (2, 4, 6, 8...)",
"bullet2": "2n+1 - all odd pages (1, 3, 5, 7...)",
"bullet3": "3n - every 3rd page (3, 6, 9, 12...)",
"bullet4": "4n-1 - pages 3, 7, 11, 15..."
},
"ranges": {
"title": "Open-ended Ranges",
"text": "Select from a starting point to the end:",
"bullet1": "5- selects pages 5 to end of document",
"bullet2": "10- selects pages 10 to end"
},
"combinations": {
"title": "Complex Combinations",
"text": "Combine different selection methods:",
"bullet1": "1,3-5,8,2n - pages 1, 3-5, 8, and all even pages",
"bullet2": "10-,2n+1 - pages 10 to end plus all odd pages",
"bullet3": "1-5,15-,3n - pages 1-5, 15 to end, and every 3rd page"
}
},
"tips": {
"title": "Tips",
"text": "Keep these guidelines in mind:",
"bullet1": "Page numbers start from 1 (not 0)",
"bullet2": "Spaces are automatically removed",
"bullet3": "Invalid expressions are ignored"
}
}
},
"compressPdfs": {
"tags": "squish,small,tiny"
@@ -747,7 +817,18 @@
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"submit": "Remove"
"submit": "Unlock Forms",
"description": "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.",
"filenamePrefix": "unlocked_forms",
"files": {
"placeholder": "Select a PDF file in the main view to get started"
},
"error": {
"failed": "An error occurred while unlocking PDF forms."
},
"results": {
"title": "Unlocked Forms Results"
}
},
"changeMetadata": {
"tags": "Title,author,date,creation,time,publisher,producer,stats",
@@ -995,7 +1076,18 @@
"title": "Remove Certificate Signature",
"header": "Remove the digital certificate from the PDF",
"selectPDF": "Select a PDF file:",
"submit": "Remove Signature"
"submit": "Remove Signature",
"description": "This tool will remove digital certificate signatures from your PDF document.",
"filenamePrefix": "unsigned",
"files": {
"placeholder": "Select a PDF file in the main view to get started"
},
"error": {
"failed": "An error occurred while removing certificate signatures."
},
"results": {
"title": "Certificate Removal Results"
}
},
"pageLayout": {
"tags": "merge,composite,single-view,organize",
@@ -1021,7 +1113,28 @@
"tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename",
"header": "Auto Rename PDF",
"submit": "Auto Rename"
"submit": "Auto Rename",
"files": {
"placeholder": "Select a PDF file in the main view to get started"
},
"error": {
"failed": "An error occurred while auto-renaming the PDF."
},
"results": {
"title": "Auto-Rename Results"
},
"tooltip": {
"header": {
"title": "How Auto-Rename Works"
},
"howItWorks": {
"title": "Smart Renaming",
"text": "Automatically finds the best title from your PDF content and uses it as the filename.",
"bullet1": "Looks for text that appears to be a title or heading",
"bullet2": "Creates a clean, valid filename from the detected title",
"bullet3": "Keeps the original name if no suitable title is found"
}
}
},
"adjust-contrast": {
"tags": "color-correction,tune,modify,enhance"
@@ -1323,7 +1436,18 @@
"pdfToSinglePage": {
"title": "PDF To Single Page",
"header": "PDF To Single Page",
"submit": "Convert To Single Page"
"submit": "Convert To Single Page",
"description": "This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.",
"filenamePrefix": "single_page",
"files": {
"placeholder": "Select a PDF file in the main view to get started"
},
"error": {
"failed": "An error occurred while converting to single page."
},
"results": {
"title": "Single Page Results"
}
},
"pageExtracter": {
"title": "Extract Pages",
@@ -1970,6 +2094,18 @@
}
}
},
"rightRail": {
"closePdf": "Close PDF",
"closeSelected": "Close Selected Files",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"selectByNumber": "Select by Page Numbers",
"deleteSelected": "Delete Selected Pages",
"toggleTheme": "Toggle Theme",
"exportAll": "Export PDF",
"downloadSelected": "Download Selected Files",
"downloadAll": "Download All"
},
"removePassword": {
"title": "Remove Password",
"desc": "Remove password protection from your PDF document.",
@@ -1991,5 +2127,20 @@
"results": {
"title": "Decrypted PDFs"
}
},
"automation": {
"suggested": {
"securePdfIngestion": "Secure PDF Ingestion",
"securePdfIngestionDesc": "Comprehensive PDF processing workflow that sanitizes documents, applies OCR with cleanup, converts to PDF/A format for long-term archival, and optimizes file size.",
"emailPreparation": "Email Preparation",
"emailPreparationDesc": "Optimizes PDFs for email distribution by compressing files, splitting large documents into 20MB chunks for email compatibility, and removing metadata for privacy.",
"secureWorkflow": "Security Workflow",
"secureWorkflowDesc": "Secures PDF documents by removing potentially malicious content like JavaScript and embedded files, then adds password protection to prevent unauthorized access. Password is set to 'password' by default.",
"processImages": "Process Images",
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
}
},
"automate": {
"copyToSaved": "Copy to Saved"
}
}
+196 -10
View File
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Reemplazar e Invertir Color",
"desc": "Reemplaza el color del texto y el fondo en el PDF e invierte el color completo del PDF para reducir el tamaño del archivo"
},
"convert": {
"title": "Convertir"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extraer Páginas"
},
"removePages": {
"title": "Eliminar",
"desc": "Eliminar páginas no deseadas del documento PDF"
},
"removeImagePdf": {
"title": "Eliminar imagen",
"desc": "Eliminar imagen del PDF> para reducir el tamaño de archivo"
},
"autoSizeSplitPDF": {
"title": "Auto dividir por tamaño/conteo",
"desc": "Divide un solo PDF en múltiples documentos según su tamaño, número de páginas, o número de documento"
},
"adjust-contrast": {
"title": "Ajustar Color/Contraste",
"desc": "Ajustar Contraste, Saturación y Brillo de un PDF"
},
"replaceColorPdf": {
"title": "Reemplazar e Invertir Color",
"desc": "Reemplaza el color del texto y el fondo en el PDF e invierte el color completo del PDF para reducir el tamaño del archivo"
},
"changePermissions": {
"title": "Cambiar permisos"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Ordenar por nombre",
"sortByDate": "Ordenar por fecha",
"removeCertSign": "¿Eliminar la firma digital en el archivo unido?",
"submit": "Unir"
"submit": "Unir",
"sortBy": {
"filename": "Nombre de Archivo"
}
},
"split": {
"tags": "Operaciones de página,dividir,Multi-página,cortar,Backend",
@@ -634,7 +669,20 @@
"8": "Documento #6: Página 10"
},
"splitPages": "Introducir las páginas para dividir:",
"submit": "Dividir"
"submit": "Dividir",
"steps": {
"settings": "Configuración"
},
"methods": {
"bySize": {
"name": "Tamaño de archivo"
}
},
"value": {
"fileSize": {
"label": "Tamaño de archivo"
}
}
},
"rotate": {
"tags": "Backend",
@@ -712,6 +760,12 @@
"type": {
"1": "Texto",
"2": "Imagen"
},
"watermarkType": {
"text": "Texto"
},
"settings": {
"fontSize": "Tamaño de Letra"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Cambiar"
},
"removePages": {
"tags": "Borrar páginas,eliminar páginas"
"tags": "Borrar páginas,eliminar páginas",
"title": "Eliminar",
"submit": "Eliminar"
},
"addPassword": {
"tags": "seguro,seguridad",
@@ -758,7 +814,12 @@
"15": "Restringir qué se puede hacer con el documento una vez abierto (no soportado por todos los lectores)",
"16": "Restringir la apertura del propio documento"
},
"submit": "Encriptar"
"submit": "Encriptar",
"tooltip": {
"permissions": {
"title": "Cambiar permisos"
}
}
},
"removePassword": {
"tags": "seguro,Desencriptar,seguridad,quitar contraseña,eliminar contraseña",
@@ -768,7 +829,12 @@
"1": "Seleccionar PDF para desencriptar",
"2": "Contraseña"
},
"submit": "Eliminar"
"submit": "Eliminar",
"desc": "Eliminar la contraseña del documento PDF",
"password": {
"stepTitle": "Eliminar contraseña",
"label": "Contraseña actual"
}
},
"compressPdfs": {
"tags": "aplastar,pequeño,diminuto"
@@ -829,7 +895,25 @@
},
"help": "Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en Docker",
"credit": "Este servicio utiliza qpdf y Tesseract para OCR",
"submit": "Procesar PDF con OCR"
"submit": "Procesar PDF con OCR",
"desc": "Tareas de limpieza y detectar texto en imágenes dentro de un PDF y volver a incrustarlo como texto",
"settings": {
"title": "Configuración",
"ocrMode": {
"label": "Modo OCR"
},
"languages": {
"label": "Idiomas"
}
},
"tooltip": {
"mode": {
"title": "Modo OCR"
},
"languages": {
"title": "Idiomas"
}
}
},
"extractImages": {
"tags": "imagen,fotografía,guardar,archivo,zip,capturar,coger",
@@ -938,7 +1022,13 @@
"title": "Aplanar",
"header": "Acoplar archivos PDF",
"flattenOnlyForms": "Aplanar sólo formularios",
"submit": "Aplanar"
"submit": "Aplanar",
"steps": {
"settings": "Configuración"
},
"options": {
"flattenOnlyForms": "Aplanar sólo formularios"
}
},
"repair": {
"tags": "reparar,restaurar,corregir,recuperar",
@@ -1174,7 +1264,22 @@
"showLayers": "Mostrar Capas (doble clic para restablecer las capas a su estado inicial)",
"colourPicker": "Selector de color",
"findCurrentOutlineItem": "Resaltar el marcador",
"applyChanges": "Aplicar cambios"
"applyChanges": "Aplicar cambios",
"auto": {
"settings": {
"advancedTitle": "Avanzado"
},
"wordsToRedact": {
"add": "Agregar"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Páginas",
"placeholder": "(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1)"
},
"export": "Exportar"
}
},
"tableExtraxt": {
"tags": "CSV,Extraer tabla,extraer,convertir"
@@ -1386,7 +1491,10 @@
"4": "Modo automático: ajusta automáticamente la calidad para que el PDF tenga el tamaño exacto",
"5": "Tamaño esperado del PDF (por ejemplo, 25 MB, 10.8 MB, 25 KB)"
},
"submit": "Comprimir"
"submit": "Comprimir",
"method": {
"filesize": "Tamaño de archivo"
}
},
"decrypt": {
"passwordPrompt": "Este archivo está protegido con contraseña. Introdúzca la contraseña:",
@@ -1557,5 +1665,83 @@
"description": "Estas cookies nos ayudan a entender cómo se están utilizando nuestras herramientas, para que podamos centrarnos en desarrollar las funciones que nuestra comunidad valora más. Tenga la seguridad de que Stirling PDF no puede y nunca podrá rastrear el contenido de los documentos con los que trabaja."
}
}
},
"download": "Descargar",
"undo": "Deshacer",
"convert": {
"title": "Convertir",
"settings": "Configuración",
"color": "Color",
"greyscale": "Escala de grises",
"fillPage": "Ocupar toda la página",
"pdfaDigitalSignatureWarning": "El PDF contiene una firma digital. Ésta se eliminará en el siguiente paso.",
"grayscale": "Escala de grises"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Seleccionar Todo",
"deselectAll": "Deseleccionar Todo"
},
"quickAccess": {
"sign": "Firmar"
},
"fileUpload": {
"loading": "Cargando...",
"or": "o"
},
"fileManager": {
"fileName": "Nombre",
"fileVersion": "Versión",
"selectAll": "Seleccionar Todo",
"deselectAll": "Deseleccionar Todo",
"deleteSelected": "Borrar selecionado(s)",
"download": "Descargar",
"delete": "Borrar"
},
"sanitize": {
"submit": "Limpiar archivo PDF",
"steps": {
"settings": "Configuración"
}
},
"changePermissions": {
"title": "Cambiar permisos",
"submit": "Cambiar permisos",
"permissions": {
"preventAssembly": {
"label": "Impedir el ensamblaje del documento"
},
"preventExtractContent": {
"label": "Impedir la extracción de contenido"
},
"preventExtractForAccessibility": {
"label": "Impedir la extracción para la accesibilidad"
},
"preventFillInForm": {
"label": "Impedir rellenar formulario"
},
"preventModify": {
"label": "Impedir modificación"
},
"preventModifyAnnotations": {
"label": "Impedir modificación de anotaciones"
},
"preventPrinting": {
"label": "Impedir imprimir"
},
"preventPrintingFaithful": {
"label": "Impedir imprimir diferentes formatos"
}
},
"tooltip": {
"header": {
"title": "Cambiar permisos"
}
}
}
}
}
+162 -31
View File
@@ -132,7 +132,7 @@
"uploadButton": "Upload Custom",
"configureButton": "Configure",
"defaultOption": "Custom",
"submitButton": "Submit",
"submitButton": "Bidali",
"help": "Pipeline Help",
"scanHelp": "Folder Scanning Help",
"deletePrompt": "Are you sure you want to delete pipeline",
@@ -147,7 +147,7 @@
"selectOperation": "Select Operation",
"addOperationButton": "Add operation",
"pipelineHeader": "Pipeline:",
"saveButton": "Download",
"saveButton": "Distira",
"validateButton": "Validate"
},
"enterpriseEdition": {
@@ -175,7 +175,7 @@
"search": "Search",
"sections": {
"organize": "Organize",
"convertTo": "Convert to PDF",
"convertTo": "PDF bihurtu",
"convertFrom": "Convert from PDF",
"security": "Sign & Security",
"advance": "Advanced",
@@ -209,10 +209,10 @@
"title": "Change Credentials",
"header": "Update Your Account Details",
"changePassword": "You are using default login credentials. Please enter a new password",
"newUsername": "New Username",
"newUsername": "Erabiltzaile izen berria",
"oldPassword": "Current Password",
"newPassword": "New Password",
"confirmNewPassword": "Confirm New Password",
"newPassword": "Pasahitz berria",
"confirmNewPassword": "Konfirmatu pasahitz berria",
"submit": "Submit Changes"
},
"account": {
@@ -578,6 +578,37 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "Bihurtu"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Atera orriak"
},
"removePages": {
"title": "Ezabatu",
"desc": "Ezabatu nahi ez dituzun orrialdeak PDF dokumentutik"
},
"removeImagePdf": {
"title": "Remove image",
"desc": "Remove image from PDF to reduce file size"
},
"autoSizeSplitPDF": {
"desc": "Split a single PDF into multiple documents based on size, page count, or document count"
},
"adjust-contrast": {
"title": "Koloreak/kontrastea doitu",
"desc": "PDF baten kontrastea, saturazioa eta distira doitzea"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "Aldatu baimenak"
}
},
"viewPdf": {
@@ -602,10 +633,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Zatitu",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "ezabatu",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -634,7 +665,10 @@
"8": "#6 Dokumentua: 10. orrialdeak"
},
"splitPages": "Sartu orrialdeak zatitzeko:",
"submit": "Zatitu"
"submit": "Zatitu",
"steps": {
"settings": "Ezarpenak"
}
},
"rotate": {
"tags": "server side",
@@ -644,10 +678,10 @@
"submit": "Biratu"
},
"imageToPdf": {
"tags": "conversion,img,jpg,picture,photo"
"tags": "conversion,img,jpg,picture,photo,psd,photoshop"
},
"pdfToImage": {
"tags": "conversion,img,jpg,picture,photo",
"tags": "conversion,img,jpg,picture,photo,psd,photoshop",
"title": "PDFa irudi bihurtu",
"header": "PDFa irudi bihurtu",
"selectText": "Irudi-formatua",
@@ -712,6 +746,9 @@
"type": {
"1": "Text",
"2": "Image"
},
"watermarkType": {
"text": "Testua"
}
},
"permissions": {
@@ -734,7 +771,9 @@
"submit": "Aldatu"
},
"removePages": {
"tags": "Remove pages,delete pages"
"tags": "Remove pages,delete pages",
"title": "Ezabatu",
"submit": "Ezabatu"
},
"addPassword": {
"tags": "secure,security",
@@ -758,7 +797,12 @@
"15": "Mugatu zer egin daitekeen dokumentuarekin behin zabalduta (Irakurle guztiek onartu gabe)",
"16": "Mugatu dokumentu bera zabaltzeko aukera"
},
"submit": "Enkriptatu"
"submit": "Enkriptatu",
"tooltip": {
"permissions": {
"title": "Aldatu baimenak"
}
}
},
"removePassword": {
"tags": "secure,Decrypt,security,unpassword,delete password",
@@ -768,7 +812,11 @@
"1": "Hautatu PDFa desenkriptatzeko",
"2": "Pasahitza"
},
"submit": "Ezabatu"
"submit": "Ezabatu",
"desc": "Ezabatu pasahitza PDF dokumentutik",
"password": {
"stepTitle": "Ezabatu pasahitza"
}
},
"compressPdfs": {
"tags": "squish,small,tiny"
@@ -829,7 +877,19 @@
},
"help": "Irakurri honen erabilerari buruzko dokumentazioa beste hizkuntza batzuetarako eta/edo ez erabili Docker-en",
"credit": "Zerbitzu honek qpdf eta OCR-rako Tesseract erabiltzen ditu",
"submit": "PDF prozesatu OCR-rekin"
"submit": "PDF prozesatu OCR-rekin",
"desc": "Garbiketa-eskaneatzeak eta irudi-testuak detektatu PDF baten barruan eta berriz ere gehitu testu gisa",
"settings": {
"title": "Ezarpenak",
"ocrMode": {
"label": "OCR modua"
}
},
"tooltip": {
"mode": {
"title": "OCR modua"
}
}
},
"extractImages": {
"tags": "picture,photo,save,archive,zip,capture,grab",
@@ -924,7 +984,7 @@
"sharedSigs": "Shared Signatures",
"noSavedSigs": "No saved signatures found",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "ezabatu",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +998,10 @@
"title": "Lautu",
"header": "Akoplatu PDF fitxategiak",
"flattenOnlyForms": "Flatten only forms",
"submit": "Lautu"
"submit": "Lautu",
"steps": {
"settings": "Ezarpenak"
}
},
"repair": {
"tags": "fix,restore,correction,recover",
@@ -1106,10 +1169,10 @@
"credit": "WeasyPrint darabil"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,web-content,transformation,convert",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Bihurtu"
},
"getPdfInfo": {
"tags": "infomation,data,stats,statistics",
@@ -1148,7 +1211,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1237,12 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"wordsToRedact": {
"add": "Gehitu"
}
}
},
"tableExtraxt": {
"tags": "CSV,Table Extraction,extract,convert"
@@ -1206,7 +1274,7 @@
"foreground": "Foreground",
"background": "Background"
},
"submit": "Submit"
"submit": "Bidali"
},
"split-by-sections": {
"tags": "Section Split, Divide, Customize",
@@ -1220,7 +1288,7 @@
"label": "Vertical Divisions",
"placeholder": "Enter number of vertical divisions"
},
"submit": "Split PDF",
"submit": "Zatitu PDFa",
"merge": "Merge Into One PDF"
},
"AddStampRequest": {
@@ -1230,16 +1298,16 @@
"stampType": "Stamp Type",
"stampText": "Stamp Text",
"stampImage": "Stamp Image",
"alphabet": "Alphabet",
"alphabet": "Alfabetoa",
"fontSize": "Font/Image Size",
"rotation": "Rotation",
"opacity": "Opacity",
"position": "Position",
"position": "Posizioa",
"overrideX": "Override X Coordinate",
"overrideY": "Override Y Coordinate",
"customMargin": "Custom Margin",
"customColor": "Custom Text Color",
"submit": "Submit"
"submit": "Bidali"
},
"removeImagePdf": {
"tags": "Remove Image,Page operations,Back end,server side"
@@ -1261,8 +1329,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Arrazoia",
"location": "Kokalekua",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1442,7 +1510,7 @@
"label": "Enter Value",
"placeholder": "Enter size (e.g., 2MB or 3KB) or count (e.g., 5)"
},
"submit": "Submit"
"submit": "Bidali"
},
"printFile": {
"title": "Print File",
@@ -1501,7 +1569,7 @@
"3": "Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.",
"4": "Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs."
},
"submit": "Split PDF"
"submit": "Zatitu PDFa"
},
"fileChooser": {
"click": "Click",
@@ -1557,5 +1625,68 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Distira",
"convert": {
"title": "Bihurtu",
"settings": "Ezarpenak",
"color": "Color",
"greyscale": "Gris-eskala",
"grayscale": "Gris-eskala"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Sinatu"
},
"fileManager": {
"fileName": "Izena",
"download": "Distira",
"delete": "ezabatu"
},
"sanitize": {
"submit": "PDF-a desinfektatu",
"steps": {
"settings": "Ezarpenak"
}
},
"changePermissions": {
"title": "Aldatu baimenak",
"submit": "Aldatu baimenak",
"permissions": {
"preventAssembly": {
"label": "Galarazi dokumentuaren mihiztaketa"
},
"preventExtractContent": {
"label": "Galarazi edukia ateratzea"
},
"preventExtractForAccessibility": {
"label": "Galarazi ateratzea irisgarritasunerako"
},
"preventFillInForm": {
"label": "Galarazi inprimakia betetzea"
},
"preventModify": {
"label": "Galarazi aldaketak egitea"
},
"preventModifyAnnotations": {
"label": "Galarazi oharrak aldatzea"
},
"preventPrinting": {
"label": "Galarazi inprimatzea"
},
"preventPrintingFaithful": {
"label": "Galarazi zenbait formatu inprimatzea"
}
},
"tooltip": {
"header": {
"title": "Aldatu baimenak"
}
}
}
}
}
+199 -14
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "در حال بارگذاری...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "جایگزینی و معکوس کردن رنگ",
"desc": "جایگزینی رنگ متن و پس‌زمینه در PDF و معکوس کردن کل رنگ‌ها برای کاهش حجم فایل"
},
"convert": {
"title": "تبدیل"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "استخراج صفحات"
},
"removePages": {
"title": "حذف",
"desc": "حذف صفحات ناخواسته از سند PDF شما."
},
"removeImagePdf": {
"title": "حذف تصویر",
"desc": "حذف تصاویر از PDF برای کاهش حجم فایل"
},
"autoSizeSplitPDF": {
"title": "تقسیم خودکار بر اساس اندازه/تعداد",
"desc": "تقسیم یک PDF به چند سند بر اساس اندازه، تعداد صفحات، یا تعداد اسناد"
},
"adjust-contrast": {
"title": "تنظیم رنگ‌ها/کنتراست",
"desc": "تنظیم کنتراست، اشباع و روشنایی یک PDF"
},
"replaceColorPdf": {
"title": "جایگزینی و معکوس کردن رنگ",
"desc": "جایگزینی رنگ متن و پس‌زمینه در PDF و معکوس کردن کل رنگ‌ها برای کاهش حجم فایل"
},
"changePermissions": {
"title": "تغییر مجوزها"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "مرتب‌سازی بر اساس نام",
"sortByDate": "مرتب‌سازی بر اساس تاریخ",
"removeCertSign": "حذف امضای دیجیتال در فایل ادغام‌شده؟",
"submit": "ادغام"
"submit": "ادغام",
"sortBy": {
"filename": "نام فایل"
}
},
"split": {
"tags": "عملیات صفحات،تقسیم،چند صفحه،برش،سمت سرور",
@@ -634,7 +669,20 @@
"8": "سند #6: صفحه ۱۰"
},
"splitPages": "صفحات برای تقسیم را وارد کنید:",
"submit": "تقسیم"
"submit": "تقسیم",
"steps": {
"settings": "تنظیمات"
},
"methods": {
"bySize": {
"name": "اندازه فایل"
}
},
"value": {
"fileSize": {
"label": "اندازه فایل"
}
}
},
"rotate": {
"tags": "سمت سرور",
@@ -712,6 +760,12 @@
"type": {
"1": "متن",
"2": "تصویر"
},
"watermarkType": {
"text": "متن"
},
"settings": {
"fontSize": "اندازه فونت"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "تغییر"
},
"removePages": {
"tags": "حذف صفحات،پاک کردن صفحات"
"tags": "حذف صفحات،پاک کردن صفحات",
"title": "حذف",
"submit": "حذف"
},
"addPassword": {
"tags": "امن،امنیت",
@@ -758,7 +814,12 @@
"15": "محدودیت‌هایی که می‌توان بر روی سند اعمال کرد هنگامی که باز است (پشتیبانی‌شده توسط همه خوانندگان نیست)",
"16": "محدودیت‌های باز شدن خود سند"
},
"submit": "رمزنگاری"
"submit": "رمزنگاری",
"tooltip": {
"permissions": {
"title": "تغییر مجوزها"
}
}
},
"removePassword": {
"tags": "امن،رمزگشایی،امنیت،حذف رمز عبور",
@@ -768,7 +829,12 @@
"1": "PDFی را برای رمزگشایی انتخاب کنید",
"2": "گذرواژه"
},
"submit": "حذف"
"submit": "حذف",
"desc": "حذف حفاظت رمز عبور از سند PDF شما.",
"password": {
"stepTitle": "حذف رمز عبور",
"label": "رمز عبور فعلی"
}
},
"compressPdfs": {
"tags": "فشرده،کوچک،ریز"
@@ -829,7 +895,25 @@
},
"help": "لطفاً این مستندات را بخوانید تا نحوه استفاده از این سرویس برای زبان‌های دیگر و/یا استفاده از آن که در داخل داکر نیست را بدانید",
"credit": "این سرویس از qpdf و Tesseract برای OCR استفاده می‌کند.",
"submit": "پردازش PDF با OCR"
"submit": "پردازش PDF با OCR",
"desc": "پاکسازی اسکن‌ها و تشخیص متن از تصاویر درون یک فایل PDF و بازافزودن آن به عنوان متن.",
"settings": {
"title": "تنظیمات",
"ocrMode": {
"label": "حالت OCR"
},
"languages": {
"label": "زبان‌ها"
}
},
"tooltip": {
"mode": {
"title": "حالت OCR"
},
"languages": {
"title": "زبان‌ها"
}
}
},
"extractImages": {
"tags": "عکس،عکس،ذخیره،آرشیو،زیپ،گرفتن،برداشتن",
@@ -938,7 +1022,13 @@
"title": "یکپارچه‌سازی",
"header": "یکپارچه‌سازی PDFها",
"flattenOnlyForms": "فقط فرم‌ها را یکپارچه کن",
"submit": "یکپارچه‌سازی"
"submit": "یکپارچه‌سازی",
"steps": {
"settings": "تنظیمات"
},
"options": {
"flattenOnlyForms": "فقط فرم‌ها را یکپارچه کن"
}
},
"repair": {
"tags": "ترمیم،بازیابی،اصلاح،بازیابی",
@@ -1106,10 +1196,10 @@
"credit": "از WeasyPrint استفاده می‌کند"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "مارک‌آپ،محتوای وب،تبدیل،تغییر",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "تبدیل"
},
"getPdfInfo": {
"tags": "اطلاعات،داده،آمار،آمارها",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,22 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "پیشرفته"
},
"wordsToRedact": {
"add": "اضافه کردن"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "صفحات",
"placeholder": "(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1)"
},
"export": "صادر کردن"
}
},
"tableExtraxt": {
"tags": "CSV، استخراج جدول، استخراج، تبدیل"
@@ -1386,7 +1491,10 @@
"4": "حالت خودکار - کیفیت را به طور خودکار تنظیم می‌کند تا PDF به اندازه دقیق برسد",
"5": "اندازه PDF مورد انتظار (مثلاً ۲۵MB، ۱۰.۸MB، ۲۵KB)"
},
"submit": "فشرده‌سازی"
"submit": "فشرده‌سازی",
"method": {
"filesize": "اندازه فایل"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "دانلود",
"convert": {
"title": "تبدیل",
"settings": "تنظیمات",
"color": "رنگ",
"greyscale": "خاکستری",
"fillPage": "پر کردن صفحه",
"pdfaDigitalSignatureWarning": "PDF حاوی یک امضای دیجیتال است. این در مرحله بعد حذف خواهد شد.",
"grayscale": "خاکستری"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "انتخاب همه",
"deselectAll": "لغو انتخاب همه"
},
"quickAccess": {
"sign": "امضا"
},
"fileUpload": {
"loading": "در حال بارگذاری...",
"or": "یا"
},
"fileManager": {
"fileName": "نام",
"fileVersion": "نسخه",
"selectAll": "انتخاب همه",
"deselectAll": "لغو انتخاب همه",
"deleteSelected": "حذف انتخاب شده‌ها",
"download": "دانلود",
"delete": "حذف"
},
"sanitize": {
"submit": "پاکسازی PDF",
"steps": {
"settings": "تنظیمات"
}
},
"changePermissions": {
"title": "تغییر مجوزها",
"submit": "تغییر مجوزها",
"permissions": {
"preventAssembly": {
"label": "جلوگیری از مونتاژ سند"
},
"preventExtractContent": {
"label": "جلوگیری از استخراج محتوا"
},
"preventExtractForAccessibility": {
"label": "جلوگیری از استخراج برای دسترسی‌پذیری"
},
"preventFillInForm": {
"label": "جلوگیری از پرکردن فرم"
},
"preventModify": {
"label": "جلوگیری از اصلاح"
},
"preventModifyAnnotations": {
"label": "جلوگیری از اصلاح حاشیه‌نویسی"
},
"preventPrinting": {
"label": "جلوگیری از چاپ"
},
"preventPrintingFaithful": {
"label": "جلوگیری از چاپ فرمت‌های مختلف"
}
},
"tooltip": {
"header": {
"title": "تغییر مجوزها"
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+197 -12
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Á lódáil...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Athchuir agus Inbhéartaigh Dath",
"desc": "Athchuir dath an téacs agus an chúlra i bhformáid PDF agus inbhéartaigh dath iomlán pdf chun méid comhaid a laghdú"
},
"convert": {
"title": "Tiontaigh"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Leathanaigh Sliocht"
},
"removePages": {
"title": "Bain",
"desc": "Scrios leathanaigh nach dteastaíonn ó do dhoiciméad PDF."
},
"removeImagePdf": {
"title": "Bain íomhá",
"desc": "Bain íomhá de PDF chun méid comhaid a laghdú"
},
"autoSizeSplitPDF": {
"title": "Auto Scoilte de réir Méid/Comhaireamh",
"desc": "Scoilt PDF amháin i ndoiciméid iolracha bunaithe ar mhéid, líon na leathanach, nó comhaireamh doiciméad"
},
"adjust-contrast": {
"title": "Coigeartaigh Dathanna/Codarsnacht",
"desc": "Coigeartaigh Codarsnacht, Sáithiú agus Gile PDF"
},
"replaceColorPdf": {
"title": "Athchuir agus Inbhéartaigh Dath",
"desc": "Athchuir dath an téacs agus an chúlra i bhformáid PDF agus inbhéartaigh dath iomlán pdf chun méid comhaid a laghdú"
},
"changePermissions": {
"title": "Athrú Ceadanna"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Sórtáil de réir ainm",
"sortByDate": "Sórtáil de réir dáta",
"removeCertSign": "Bain síniú digiteach sa chomhad cumaiscthe?",
"submit": "Cumaisc"
"submit": "Cumaisc",
"sortBy": {
"filename": "Ainm comhaid"
}
},
"split": {
"tags": "Oibríochtaí leathanach, roinnt, Leathanach Il, gearrtha, taobh freastalaí",
@@ -634,7 +669,20 @@
"8": "Doiciméad #6: Leathanach 10"
},
"splitPages": "Iontráil leathanaigh le scoilt ar:",
"submit": "Scoilt"
"submit": "Scoilt",
"steps": {
"settings": "Socruithe"
},
"methods": {
"bySize": {
"name": "Méid an Chomhaid"
}
},
"value": {
"fileSize": {
"label": "Méid an Chomhaid"
}
}
},
"rotate": {
"tags": "taobh freastalaí",
@@ -712,6 +760,12 @@
"type": {
"1": "Téacs",
"2": "Íomha"
},
"watermarkType": {
"text": "Téacs"
},
"settings": {
"fontSize": "Méid an Chló"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Athrú"
},
"removePages": {
"tags": "Bain leathanaigh, scrios leathanaigh"
"tags": "Bain leathanaigh, scrios leathanaigh",
"title": "Bain",
"submit": "Bain"
},
"addPassword": {
"tags": "slán, slándáil",
@@ -758,7 +814,12 @@
"15": "Cuireann sé srian lenar féidir a dhéanamh leis an doiciméad nuair a osclaítear é (Ní thacaíonn gach léitheoir leis)",
"16": "Cuireann sé srian le hoscailt an doiciméid féin"
},
"submit": "Criptigh"
"submit": "Criptigh",
"tooltip": {
"permissions": {
"title": "Athrú Ceadanna"
}
}
},
"removePassword": {
"tags": "slán, Díchriptiú, slándáil, Unpassword, scrios pasfhocal",
@@ -768,7 +829,12 @@
"1": "Roghnaigh PDF le Díchriptiú",
"2": "Pasfhocal"
},
"submit": "Bain"
"submit": "Bain",
"desc": "Bain cosaint phasfhocal ó do dhoiciméad PDF.",
"password": {
"stepTitle": "Bain Pasfhocal",
"label": "Pasfhocal reatha"
}
},
"compressPdfs": {
"tags": "squish, beag, beag bídeach"
@@ -829,7 +895,25 @@
},
"help": "Léigh le do thoil an doiciméadú seo ar conas é seo a úsáid do theangacha eile agus/nó úsáid nach bhfuil i ndugairí",
"credit": "Úsáideann an tseirbhís seo qpdf agus Tesseract le haghaidh OCR.",
"submit": "Próiseáil PDF le OCR"
"submit": "Próiseáil PDF le OCR",
"desc": "Scanann glantachán agus aimsíonn sé téacs ó íomhánna laistigh de PDF agus cuireann sé isteach arís é mar théacs.",
"settings": {
"title": "Socruithe",
"ocrMode": {
"label": "Mód OCR"
},
"languages": {
"label": "Teangacha"
}
},
"tooltip": {
"mode": {
"title": "Mód OCR"
},
"languages": {
"title": "Teangacha"
}
}
},
"extractImages": {
"tags": "pictiúr, grianghraf, shábháil, cartlann, zip, gabháil, grab",
@@ -935,10 +1019,16 @@
},
"flatten": {
"tags": "statach, díghníomhachtú, neamh-idirghníomhach, sruthlíniú",
"title": "Flatten",
"title": "Comhcheangail",
"header": "PDF cothromú",
"flattenOnlyForms": "Flatten foirmeacha amháin",
"submit": "Flatten"
"submit": "Comhcheangail",
"steps": {
"settings": "Socruithe"
},
"options": {
"flattenOnlyForms": "Flatten foirmeacha amháin"
}
},
"repair": {
"tags": "deisiú, athchóiriú, ceartú, aisghabháil",
@@ -1174,7 +1264,22 @@
"showLayers": "Taispeáin Sraitheanna (cliceáil faoi dhó chun gach sraith a athshocrú go dtí an staid réamhshocraithe)",
"colourPicker": "Roghnóir Dathanna",
"findCurrentOutlineItem": "Faigh imlíne reatha",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Casta"
},
"wordsToRedact": {
"add": "Cuir"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Leathanaigh",
"placeholder": "(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1)"
},
"export": "Easpórtáil"
}
},
"tableExtraxt": {
"tags": "CSV, Eastóscadh Tábla, sliocht, tiontú"
@@ -1386,7 +1491,10 @@
"4": "Mód uathoibríoch - Coigeartaíonn Auto cáilíocht chun PDF a fháil go dtí an méid cruinn",
"5": "Méid PDF a bhfuiltear ag súil leis (m.sh. 25MB, 10.8MB, 25KB)"
},
"submit": "Comhbhrúigh"
"submit": "Comhbhrúigh",
"method": {
"filesize": "Méid an Chomhaid"
}
},
"decrypt": {
"passwordPrompt": "Tá an comhad seo cosanta ag pasfhocal. Cuir isteach an pasfhocal le do thoil:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Íosluchtaigh",
"convert": {
"title": "Tiontaigh",
"settings": "Socruithe",
"color": "Dath",
"greyscale": "Scála Liath",
"fillPage": "Líon Leathanach",
"pdfaDigitalSignatureWarning": "Tá síniú digiteach ar an PDF. Bainfear é seo sa chéad chéim eile.",
"grayscale": "Scála Liath"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Roghnaigh Uile",
"deselectAll": "Díroghnaigh Uile"
},
"quickAccess": {
"sign": "Comhartha"
},
"fileUpload": {
"loading": "Á lódáil...",
"or": "nó"
},
"fileManager": {
"fileName": "Ainm",
"fileVersion": "Leagan",
"selectAll": "Roghnaigh Uile",
"deselectAll": "Díroghnaigh Uile",
"deleteSelected": "Scrios Roghnaithe",
"download": "Íosluchtaigh",
"delete": "Scrios"
},
"sanitize": {
"submit": "PDF sláintíocht",
"steps": {
"settings": "Socruithe"
}
},
"changePermissions": {
"title": "Athrú Ceadanna",
"submit": "Athrú Ceadanna",
"permissions": {
"preventAssembly": {
"label": "Cosc a chur le chéile doiciméad"
},
"preventExtractContent": {
"label": "Cosc a chur ar eastóscadh ábhar"
},
"preventExtractForAccessibility": {
"label": "Cosc a chur ar eastóscadh le haghaidh inrochtaineachta"
},
"preventFillInForm": {
"label": "Cosc ar fhoirm a líonadh"
},
"preventModify": {
"label": "Cosc a chur ar mhodhnú"
},
"preventModifyAnnotations": {
"label": "Cosc a chur ar mhodhnú anótála"
},
"preventPrinting": {
"label": "Cosc a chur ar phriontáil"
},
"preventPrintingFaithful": {
"label": "Cosc a chur ar phriontáil bhformáidí éagsúla"
}
},
"tooltip": {
"header": {
"title": "Athrú Ceadanna"
}
}
}
}
}
+198 -13
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "लोड हो रहा है...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "रंग बदलें और उल्टा करें",
"desc": "PDF में टेक्स्ट और पृष्ठभूमि के लिए रंग बदलें और फ़ाइल आकार कम करने के लिए पूर्ण रंग को उल्टा करें"
},
"convert": {
"title": "बदलें"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "पृष्ठ निकालें"
},
"removePages": {
"title": "निकालें",
"desc": "अपने PDF दस्तावेज से अवांछित पृष्ठ हटाएं।"
},
"removeImagePdf": {
"title": "छवि हटाएं",
"desc": "फ़ाइल आकार कम करने के लिए PDF से छवि हटाएं"
},
"autoSizeSplitPDF": {
"title": "आकार/संख्या के आधार पर स्वतः विभाजित करें",
"desc": "एक PDF को आकार, पृष्ठ संख्या, या दस्तावेज़ संख्या के आधार पर कई दस्तावेज़ों में विभाजित करें"
},
"adjust-contrast": {
"title": "रंग/कंट्रास्ट समायोजित करें",
"desc": "PDF का कंट्रास्ट, संतृप्ति और चमक समायोजित करें"
},
"replaceColorPdf": {
"title": "रंग बदलें और उल्टा करें",
"desc": "PDF में टेक्स्ट और पृष्ठभूमि के लिए रंग बदलें और फ़ाइल आकार कम करने के लिए पूर्ण रंग को उल्टा करें"
},
"changePermissions": {
"title": "अनुमतियां बदलें"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "नाम से क्रमबद्ध करें",
"sortByDate": "तिथि से क्रमबद्ध करें",
"removeCertSign": "मर्ज की गई फ़ाइल में डिजिटल हस्ताक्षर हटाएं?",
"submit": "मर्ज करें"
"submit": "मर्ज करें",
"sortBy": {
"filename": "फ़ाइल नाम"
}
},
"split": {
"tags": "पेज ऑपरेशन्स,डिवाइड,मल्टी पेज,कट,सर्वर साइड",
@@ -634,7 +669,20 @@
"8": "दस्तावेज़ #6: पृष्ठ 10"
},
"splitPages": "विभाजन करने के लिए पृष्ठ दर्ज करें:",
"submit": "विभाजित करें"
"submit": "विभाजित करें",
"steps": {
"settings": "सेटिंग्स"
},
"methods": {
"bySize": {
"name": "फ़ाइल आकार"
}
},
"value": {
"fileSize": {
"label": "फ़ाइल आकार"
}
}
},
"rotate": {
"tags": "सर्वर साइड",
@@ -712,6 +760,12 @@
"type": {
"1": "टेक्स्ट",
"2": "छवि"
},
"watermarkType": {
"text": "टेक्स्ट"
},
"settings": {
"fontSize": "फ़ॉन्ट आकार"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "बदलें"
},
"removePages": {
"tags": "पृष्ठ निकालें,पृष्ठ हटाएं"
"tags": "पृष्ठ निकालें,पृष्ठ हटाएं",
"title": "निकालें",
"submit": "निकालें"
},
"addPassword": {
"tags": "सुरक्षित,सुरक्षा",
@@ -758,7 +814,12 @@
"15": "एक बार खुलने के बाद दस्तावेज़ के साथ क्या किया जा सकता है को प्रतिबंधित करता है (सभी पाठकों द्वारा समर्थित नहीं)",
"16": "दस्तावेज़ को खोलने को प्रतिबंधित करता है"
},
"submit": "एन्क्रिप्ट करें"
"submit": "एन्क्रिप्ट करें",
"tooltip": {
"permissions": {
"title": "अनुमतियां बदलें"
}
}
},
"removePassword": {
"tags": "सुरक्षित,डिक्रिप्ट,सुरक्षा,अनपासवर्ड,पासवर्ड हटाएं",
@@ -768,7 +829,12 @@
"1": "डिक्रिप्ट करने के लिए PDF चुनें",
"2": "पासवर्ड"
},
"submit": "हटाएं"
"submit": "हटाएं",
"desc": "अपने PDF दस्तावेज से पासवर्ड सुरक्षा हटाएं।",
"password": {
"stepTitle": "पासवर्ड हटाएं",
"label": "वर्तमान पासवर्ड"
}
},
"compressPdfs": {
"tags": "कम्प्रेस,छोटा,छोटा"
@@ -829,7 +895,25 @@
},
"help": "कृपया अन्य भाषाओं के लिए उपयोग और/या डॉकर में उपयोग न करने के बारे में यह दस्तावेज़ीकरण पढ़ें",
"credit": "यह सेवा OCR के लिए qpdf और Tesseract का उपयोग करती है।",
"submit": "OCR के साथ PDF प्रोसेस करें"
"submit": "OCR के साथ PDF प्रोसेस करें",
"desc": "स्कैन को साफ करें और PDF के अंदर छवियों से टेक्स्ट का पता लगाएं और उसे टेक्स्ट के रूप में फिर से जोड़ें।",
"settings": {
"title": "सेटिंग्स",
"ocrMode": {
"label": "OCR मोड"
},
"languages": {
"label": "भाषाएं"
}
},
"tooltip": {
"mode": {
"title": "OCR मोड"
},
"languages": {
"title": "भाषाएं"
}
}
},
"extractImages": {
"tags": "चित्र,फोटो,सहेजें,संग्रह,ज़िप,कैप्चर,ग्रैब",
@@ -938,7 +1022,13 @@
"title": "समतल करें",
"header": "PDF समतल करें",
"flattenOnlyForms": "केवल फ़ॉर्म समतल करें",
"submit": "समतल करें"
"submit": "समतल करें",
"steps": {
"settings": "सेटिंग्स"
},
"options": {
"flattenOnlyForms": "केवल फ़ॉर्म समतल करें"
}
},
"repair": {
"tags": "ठीक करें,पुनर्स्थापित करें,सुधार,पुनर्प्राप्त करें",
@@ -1106,10 +1196,10 @@
"credit": "WeasyPrint का उपयोग करता है"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "मार्कअप,वेब-सामग्री,रूपांतरण,बदलें",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "बदलें"
},
"getPdfInfo": {
"tags": "जानकारी,डेटा,आँकड़े,सांख्यिकी",
@@ -1174,7 +1264,22 @@
"showLayers": "लेयर्स दिखाएं (सभी लेयर्स को डिफ़ॉल्ट स्थिति में रीसेट करने के लिए डबल-क्लिक करें)",
"colourPicker": "रंग चयनकर्ता",
"findCurrentOutlineItem": "वर्तमान आउटलाइन आइटम खोजें",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "उन्नत"
},
"wordsToRedact": {
"add": "जोड़ें"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "पृष्ठ",
"placeholder": "(जैसे 1,2,8 या 4,7,12-16 या 2n-1)"
},
"export": "निर्यात करें"
}
},
"tableExtraxt": {
"tags": "CSV,तालिका निष्कर्षण,निकालें,बदलें"
@@ -1386,7 +1491,10 @@
"4": "स्वतः मोड - PDF को सटीक आकार प्राप्त करने के लिए गुणवत्ता को स्वतः समायोजित करता है",
"5": "अपेक्षित PDF आकार (जैसे 25MB, 10.8MB, 25KB)"
},
"submit": "कम्प्रेस करें"
"submit": "कम्प्रेस करें",
"method": {
"filesize": "फ़ाइल आकार"
}
},
"decrypt": {
"passwordPrompt": "यह फ़ाइल पासवर्ड से सुरक्षित है। कृपया पासवर्ड दर्ज करें:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "डाउनलोड करें",
"convert": {
"title": "बदलें",
"settings": "सेटिंग्स",
"color": "रंग",
"greyscale": "ग्रेस्केल",
"fillPage": "पृष्ठ भरें",
"pdfaDigitalSignatureWarning": "PDF में एक डिजिटल हस्ताक्षर है। यह अगले चरण में हटा दिया जाएगा।",
"grayscale": "ग्रेस्केल"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "सभी चुनें",
"deselectAll": "सभी अचयनित करें"
},
"quickAccess": {
"sign": "हस्ताक्षर करें"
},
"fileUpload": {
"loading": "लोड हो रहा है...",
"or": "या"
},
"fileManager": {
"fileName": "नाम",
"fileVersion": "संस्करण",
"selectAll": "सभी चुनें",
"deselectAll": "सभी अचयनित करें",
"deleteSelected": "चयनित हटाएं",
"download": "डाउनलोड करें",
"delete": "हटाएं"
},
"sanitize": {
"submit": "PDF सैनिटाइज़ करें",
"steps": {
"settings": "सेटिंग्स"
}
},
"changePermissions": {
"title": "अनुमतियां बदलें",
"submit": "अनुमतियां बदलें",
"permissions": {
"preventAssembly": {
"label": "दस्तावेज़ असेंबली को रोकें"
},
"preventExtractContent": {
"label": "सामग्री निष्कर्षण को रोकें"
},
"preventExtractForAccessibility": {
"label": "पहुंच योग्यता के लिए निष्कर्षण को रोकें"
},
"preventFillInForm": {
"label": "फॉर्म भरने को रोकें"
},
"preventModify": {
"label": "संशोधन को रोकें"
},
"preventModifyAnnotations": {
"label": "टिप्पणी संशोधन को रोकें"
},
"preventPrinting": {
"label": "प्रिंटिंग को रोकें"
},
"preventPrintingFaithful": {
"label": "विभिन्न प्रारूपों में प्रिंटिंग को रोकें"
}
},
"tooltip": {
"header": {
"title": "अनुमतियां बदलें"
}
}
}
}
}
+198 -22
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Učitavanje...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -476,7 +476,7 @@
"desc": "Promijenite veličinu/razmjer stranice i/ili njezin sadržaj."
},
"pipeline": {
"title": "Pipeline",
"title": "Tok rada",
"desc": "Izvršite više radnji na PDF-ovima definiranjem skripti u pipeline-u"
},
"addPageNumbers": {
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Zamenite boju teksta i pozadine u PDF-u te inverzirajte cijeli PDF kako bi se smanjila veličina datoteke."
},
"convert": {
"title": "Pretvori"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Izdvojiti stranice"
},
"removePages": {
"title": "Ukloniti",
"desc": "Izbrišite neželjene stranice iz svog PDF dokumenta."
},
"removeImagePdf": {
"title": "Ukloni sliku",
"desc": "Ukloni sliku iz PDF-a kako bi se smanjio veličina datoteke"
},
"autoSizeSplitPDF": {
"title": "Automatska podjela po veličini/broju",
"desc": "Podijelite jedan PDF na više dokumenata na temelju veličine, broja stranica ili broja dokumenata"
},
"adjust-contrast": {
"title": "Podesi boje/kontrast",
"desc": "Podesite kontrast, zasićenost i svjetlinu PDF-a"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Zamenite boju teksta i pozadine u PDF-u te inverzirajte cijeli PDF kako bi se smanjila veličina datoteke."
},
"changePermissions": {
"title": "Promjena dopuštenja"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "Stranica",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Razdvajanje",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Izbriši",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Poredaj po imenu",
"sortByDate": "Poredaj po datumu",
"removeCertSign": "Ukloniti digitalni potpis u kombiniranom datoteku?",
"submit": "Spajanje"
"submit": "Spajanje",
"sortBy": {
"filename": "Ime datoteke"
}
},
"split": {
"tags": "Operacije stranice, dijeljenje, više stranica, rezanje,poslužiteljska strana",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Stranice 10"
},
"splitPages": "Unesite stranice za razdvajanje:",
"submit": "Razdvoji"
"submit": "Razdvoji",
"steps": {
"settings": "Postavke"
},
"methods": {
"bySize": {
"name": "Veličina datoteke"
}
},
"value": {
"fileSize": {
"label": "Veličina datoteke"
}
}
},
"rotate": {
"tags": "poslužiteljska strana",
@@ -712,6 +760,12 @@
"type": {
"1": "Tekst",
"2": "Slika"
},
"watermarkType": {
"text": "Tekst"
},
"settings": {
"fontSize": "Veličina pisma"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Promijeniti"
},
"removePages": {
"tags": "Ukloni stranice,izbriši stranice"
"tags": "Ukloni stranice,izbriši stranice",
"title": "Ukloniti",
"submit": "Ukloniti"
},
"addPassword": {
"tags": "sigurno, sigurnost",
@@ -758,7 +814,12 @@
"15": "Ograničava što se može učiniti s dokumentom nakon što se otvori (ne podržavaju svi čitači)",
"16": "Ograničava otvaranje samog dokumenta"
},
"submit": "Šifriraj"
"submit": "Šifriraj",
"tooltip": {
"permissions": {
"title": "Promjena dopuštenja"
}
}
},
"removePassword": {
"tags": "sigurno, dešifriranje, sigurnost, poništi lozinku, izbriši lozinku",
@@ -768,7 +829,12 @@
"1": "Odaberite PDF za dekriptiranje",
"2": "Zaporka"
},
"submit": "Ukloniti"
"submit": "Ukloniti",
"desc": "Uklonite zaštitu lozinkom sa svog PDF dokumenta..",
"password": {
"stepTitle": "Ukloni lozinku",
"label": "Trenutna zaporka"
}
},
"compressPdfs": {
"tags": "squish, mali, maleni"
@@ -829,7 +895,25 @@
},
"help": "Pročitajte ovu dokumentaciju o tome kako ovo koristiti za druge jezike i/ili koristiti ne u dockeru",
"credit": "Ova usluga koristi qpdf i Tesseract za OCR.",
"submit": "Obradi PDF sa OCR-om"
"submit": "Obradi PDF sa OCR-om",
"desc": "Čišćenje skenira i otkriva tekst sa slika unutar PDF-a i ponovno ga dodaje kao tekst.",
"settings": {
"title": "Postavke",
"ocrMode": {
"label": "OCR način"
},
"languages": {
"label": "Jezici"
}
},
"tooltip": {
"mode": {
"title": "OCR način"
},
"languages": {
"title": "Jezici"
}
}
},
"extractImages": {
"tags": "slika, fotografija, spremanje, arhiva, zip, snimanje, zgrabi",
@@ -924,7 +1008,7 @@
"sharedSigs": "Dijeljeni potpisi",
"noSavedSigs": "Nema sacuvanih potpisa pronađenih",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Izbriši",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "Izravnati",
"header": "Izravnati pdf",
"flattenOnlyForms": "Izravnati samo obrasce",
"submit": "Izravnati"
"submit": "Izravnati",
"steps": {
"settings": "Postavke"
},
"options": {
"flattenOnlyForms": "Izravnati samo obrasce"
}
},
"repair": {
"tags": "popravi,vrati,korekcija,obnovi",
@@ -1106,10 +1196,10 @@
"credit": "Koristi WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "oznake,web-sadržaj,transformacija,konvertiranje",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Pretvori"
},
"getPdfInfo": {
"tags": "informacije,podaci,statistike",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,21 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Napredno"
},
"wordsToRedact": {
"add": "Dodaj"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Stranice",
"placeholder": "(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,Izdvajanje tabela,izdvajanje,pretvaranje"
@@ -1261,8 +1365,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Razlog",
"location": "Mjesto",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1385,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Verzija",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1490,10 @@
"4": "Automatski način - Automatski prilagođava kvalitetu kako bi PDF dobio točnu veličinu",
"5": "Očekivana veličina PDF-a (npr. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Kompresiraj"
"submit": "Kompresiraj",
"method": {
"filesize": "Veličina datoteke"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1664,74 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Preuzmi datoteku",
"convert": {
"title": "Pretvori",
"settings": "Postavke",
"color": "Boja",
"greyscale": "Sivi tonovi",
"fillPage": "Ispuni stranicu",
"pdfaDigitalSignatureWarning": "PDF sadrži digitalni potpis. U sledećem koraku će biti uklonjen.",
"grayscale": "Sivi tonovi"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Potpisati"
},
"fileUpload": {
"loading": "Učitavanje..."
},
"fileManager": {
"fileName": "Ime",
"fileVersion": "Verzija",
"download": "Preuzmi datoteku",
"delete": "Izbriši"
},
"sanitize": {
"submit": "Sanirajte PDF",
"steps": {
"settings": "Postavke"
}
},
"changePermissions": {
"title": "Promjena dopuštenja",
"submit": "Promjena dopuštenja",
"permissions": {
"preventAssembly": {
"label": "Spriječiti sastavljanje dokumenta"
},
"preventExtractContent": {
"label": "Spriječiti izdvajanje sadržaja"
},
"preventExtractForAccessibility": {
"label": "Spriječite izvlačenje radi pristupačnosti"
},
"preventFillInForm": {
"label": "Spriječiti ispunjavanje obrasca"
},
"preventModify": {
"label": "Spriječiti izmjene"
},
"preventModifyAnnotations": {
"label": "Spriječi modificiranje napomena"
},
"preventPrinting": {
"label": "Spriječiti ispis"
},
"preventPrintingFaithful": {
"label": "Spriječite ispis različitih formata"
}
},
"tooltip": {
"header": {
"title": "Promjena dopuštenja"
}
}
}
}
}
+205 -14
View File
@@ -25,7 +25,7 @@
"multiPdfDropPrompt": "Válassza ki (vagy húzza ide) az összes szükséges PDF-fájlt",
"imgPrompt": "Kép kiválasztása",
"genericSubmit": "Küldés",
"uploadLimit": "Maximum file size:",
"uploadLimit": "Maximális fájlméret:",
"uploadLimitExceededSingular": "túl nagy. A maximálisan megengedett méret",
"uploadLimitExceededPlural": "túl nagyok. A maximálisan megengedett méretek",
"processTimeWarning": "Figyelmeztetés: A folyamat akár egy percig is eltarthat a fájlmérettől függően",
@@ -166,7 +166,7 @@
},
"navbar": {
"favorite": "Kedvencek",
"recent": "New and recently updated",
"recent": "Új és nemrég frissített",
"darkmode": "Sötét mód",
"language": "Nyelvek",
"settings": "Beállítások",
@@ -578,6 +578,43 @@
"replace-color": {
"title": "Színek cseréje és invertálása",
"desc": "PDF szöveg és háttérszíneinek cseréje és teljes színinvertálás a fájlméret csökkentése érdekében"
},
"convert": {
"title": "Konvertálás"
},
"attachments": {
"title": "Csatolmányok hozzáadása a PDF-hez",
"desc": "Csatolmányok (beágyazott fájlok) hozzáadása vagy eltávolítása a PDF-ből"
},
"editTableOfContents": {
"title": "Szerkesztés vagy hozzáadás a PDF tartalomjegyzékéhez",
"desc": "PDF dokumentumokban könyvjelzők és tartalomjegyzék hozzáadása vagy szerkesztése"
},
"extractPages": {
"title": "Oldalak kinyerése"
},
"removePages": {
"title": "Eltávolítás",
"desc": "Felesleges oldalak törlése a PDF dokumentumból."
},
"removeImagePdf": {
"title": "Képek eltávolítása",
"desc": "Képek eltávolítása PDF-ből a fájlméret csökkentése érdekében"
},
"autoSizeSplitPDF": {
"title": "Automatikus felosztás méret/darabszám szerint",
"desc": "Egyetlen PDF felosztása több dokumentumra méret, oldalszám vagy dokumentumszám alapján"
},
"adjust-contrast": {
"title": "Színek/kontraszt beállítása",
"desc": "PDF kontraszt, telítettség és fényerő beállítása"
},
"replaceColorPdf": {
"title": "Színek cseréje és invertálása",
"desc": "PDF szöveg és háttérszíneinek cseréje és teljes színinvertálás a fájlméret csökkentése érdekében"
},
"changePermissions": {
"title": "Jogosultságok módosítása"
}
},
"viewPdf": {
@@ -617,7 +654,10 @@
"sortByName": "Rendezés név szerint",
"sortByDate": "Rendezés dátum szerint",
"removeCertSign": "Digitális aláírás eltávolítása az egyesített fájlban?",
"submit": "Egyesítés"
"submit": "Egyesítés",
"sortBy": {
"filename": "Fájlnév"
}
},
"split": {
"tags": "Oldalműveletek,felosztás,Több oldal,vágás,szerver oldali",
@@ -634,7 +674,20 @@
"8": "6. dokumentum: 10. oldal"
},
"splitPages": "Adja meg a felosztási pontokat:",
"submit": "Felosztás"
"submit": "Felosztás",
"steps": {
"settings": "Beállítások"
},
"methods": {
"bySize": {
"name": "Fájlméret"
}
},
"value": {
"fileSize": {
"label": "Fájlméret"
}
}
},
"rotate": {
"tags": "szerver oldali",
@@ -712,6 +765,12 @@
"type": {
"1": "Szöveg",
"2": "Kép"
},
"watermarkType": {
"text": "Szöveg"
},
"settings": {
"fontSize": "Betűméret"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "Módosítás"
},
"removePages": {
"tags": "Oldalak eltávolítása,oldalak törlése"
"tags": "Oldalak eltávolítása,oldalak törlése",
"title": "Eltávolítás",
"submit": "Eltávolítás"
},
"addPassword": {
"tags": "biztonság,védelem",
@@ -758,7 +819,12 @@
"15": "Korlátozza, hogy mi végezhető el a dokumentum megnyitása után (Nem minden olvasó támogatja)",
"16": "Korlátozza a dokumentum megnyithatságát"
},
"submit": "Titkosítás"
"submit": "Titkosítás",
"tooltip": {
"permissions": {
"title": "Jogosultságok módosítása"
}
}
},
"removePassword": {
"tags": "biztonság,feloldás,védelem,jelszó törlése",
@@ -768,7 +834,12 @@
"1": "Válassza ki a visszafejtendő PDF-et",
"2": "Jelszó"
},
"submit": "Eltávolítás"
"submit": "Eltávolítás",
"desc": "Jelszavas védelem eltávolítása a PDF dokumentumból",
"password": {
"stepTitle": "Jelszó eltávolítása",
"label": "Jelenlegi jelszó"
}
},
"compressPdfs": {
"tags": "tömörítés,kicsi,kompakt"
@@ -829,7 +900,25 @@
},
"help": "Kérjük, olvassa el ezt a dokumentációt más nyelvek használatáról és/vagy nem Docker környezetben való használatról",
"credit": "Ez a szolgáltatás a qpdf és Tesseract OCR használatával működik.",
"submit": "PDF feldolgozása OCR-rel"
"submit": "PDF feldolgozása OCR-rel",
"desc": "Szkennelt dokumentumok tisztítása és szövegfelismerés képekből, majd visszaadása szerkeszthető szövegként",
"settings": {
"title": "Beállítások",
"ocrMode": {
"label": "OCR mód"
},
"languages": {
"label": "Nyelvek"
}
},
"tooltip": {
"mode": {
"title": "OCR mód"
},
"languages": {
"title": "Nyelvek"
}
}
},
"extractImages": {
"tags": "kép,fotó,mentés,archívum,tömörítés,kinyerés,gyűjtés",
@@ -930,15 +1019,21 @@
"next": "Következő oldal",
"previous": "Előző oldal",
"maintainRatio": "Képarány fenntartása váltása",
"undo": "Undo",
"redo": "Redo"
"undo": "Visszavonás",
"redo": "Újra"
},
"flatten": {
"tags": "statikus,deaktiválás,nem interaktív,egyszerűsítés",
"title": "Lapítás",
"header": "PDF-ek lapítása",
"flattenOnlyForms": "Csak űrlapok lapítása",
"submit": "Lapítás"
"submit": "Lapítás",
"steps": {
"settings": "Beállítások"
},
"options": {
"flattenOnlyForms": "Csak űrlapok lapítása"
}
},
"repair": {
"tags": "javítás,helyreállítás,korrekció,visszaállítás",
@@ -1174,7 +1269,22 @@
"showLayers": "Rétegek megjelenítése (dupla kattintás az összes réteg alaphelyzetbe állításához)",
"colourPicker": "Színválasztó",
"findCurrentOutlineItem": "Keresés a jelenlegi vázlatban",
"applyChanges": "Változtatások mentése"
"applyChanges": "Változtatások mentése",
"auto": {
"settings": {
"advancedTitle": "Haladó"
},
"wordsToRedact": {
"add": "Hozzáadás"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Oldal",
"placeholder": "(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1)"
},
"export": "Exportálás"
}
},
"tableExtraxt": {
"tags": "CSV,Táblázat kinyerése,kinyerés,konvertálás"
@@ -1386,7 +1496,10 @@
"4": "Automatikus mód - Automatikusan állítja a minőséget a megadott PDF méret eléréséhez",
"5": "Kívánt PDF méret (pl. 25MB, 10.8MB, 25KB)"
},
"submit": "Tömörítés"
"submit": "Tömörítés",
"method": {
"filesize": "Fájlméret"
}
},
"decrypt": {
"passwordPrompt": "Ez a fájl jelszóval védett. Kérjük, adja meg a jelszót:",
@@ -1557,5 +1670,83 @@
"description": "Ezek a sütik segítenek megérteni, hogyan használják eszközeinket, így a közösségünk által leginkább értékelt funkciókra összpontosíthatunk. Nyugodt lehet-a Stirling PDF nem képes és soha nem is fog nyomon követni az Ön által használt dokumentumok tartalmát."
}
}
},
"download": "Letöltés",
"undo": "Visszavonás",
"convert": {
"title": "Konvertálás",
"settings": "Beállítások",
"color": "Szín",
"greyscale": "Szürkeárnyalatos",
"fillPage": "Oldal kitöltése",
"pdfaDigitalSignatureWarning": "A PDF digitális aláírást tartalmaz. Ez a következő lépésben eltávolításra kerül.",
"grayscale": "Szürkeárnyalatos"
},
"attachments": {
"tags": "beágyazás,csatolás,fájl,csatolmány,csatolmányok",
"title": "Mellékletek hozzáadása",
"header": "Mellékletek hozzáadása",
"submit": "Csatolmányok hozzáadása a PDF-hez"
},
"rightRail": {
"selectAll": "Összes kijelölése",
"deselectAll": "Kijelölés megszüntetése"
},
"quickAccess": {
"sign": "Aláírás"
},
"fileUpload": {
"loading": "Betöltés...",
"or": "vagy"
},
"fileManager": {
"fileName": "Név",
"fileVersion": "Verzió",
"selectAll": "Összes kijelölése",
"deselectAll": "Kijelölés megszüntetése",
"deleteSelected": "Kijelöltek törlése",
"download": "Letöltés",
"delete": "Törlés"
},
"sanitize": {
"submit": "PDF tisztítása",
"steps": {
"settings": "Beállítások"
}
},
"changePermissions": {
"title": "Jogosultságok módosítása",
"submit": "Jogosultságok módosítása",
"permissions": {
"preventAssembly": {
"label": "Dokumentum egyesítésének megakadályozása"
},
"preventExtractContent": {
"label": "Tartalom kinyerésének megakadályozása"
},
"preventExtractForAccessibility": {
"label": "Akadálymentesítési célú kinyerés megakadályozása"
},
"preventFillInForm": {
"label": "Űrlapkitöltés megakadályozása"
},
"preventModify": {
"label": "Módosítás megakadályozása"
},
"preventModifyAnnotations": {
"label": "Megjegyzések módosításának megakadályozása"
},
"preventPrinting": {
"label": "Nyomtatás megakadályozása"
},
"preventPrintingFaithful": {
"label": "Különböző formátumú nyomtatás megakadályozása"
}
},
"tooltip": {
"header": {
"title": "Jogosultságok módosítása"
}
}
}
}
}
+197 -21
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Mengambil data...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Ganti dan Inversi Warna",
"desc": "Ganti warna untuk teks dan latar belakang dalam PDF dan inversi seluruh warna PDF untuk mengurangi ukuran file"
},
"convert": {
"title": "Konversi"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Ekstrak Halaman"
},
"removePages": {
"title": "Menghapus",
"desc": "Menghapus halaman yang tidak diinginkan dari dokumen PDF Anda."
},
"removeImagePdf": {
"title": "Hapus Gambar",
"desc": "Hapus gambar dari PDF untuk mengurangi ukuran file"
},
"autoSizeSplitPDF": {
"title": "Pemisahan Otomatis berdasarkan Ukuran/Hitungan",
"desc": "Membagi satu PDF menjadi beberapa dokumen berdasarkan ukuran, jumlah halaman, atau jumlah dokumen"
},
"adjust-contrast": {
"title": "Menyesuaikan Warna/Kontras",
"desc": "Sesuaikan Kontras, Saturasi, dan Kecerahan PDF"
},
"replaceColorPdf": {
"title": "Ganti dan Inversi Warna",
"desc": "Ganti warna untuk teks dan latar belakang dalam PDF dan inversi seluruh warna PDF untuk mengurangi ukuran file"
},
"changePermissions": {
"title": "Ganti Perizinan"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "Halaman",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Membagi",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Hapus",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sortir berdasarkan nama",
"sortByDate": "Sortir berdasrkan tanggal",
"removeCertSign": "Hapus tanda tangan digital dalam file yang dicampur?",
"submit": "Gabungkan"
"submit": "Gabungkan",
"sortBy": {
"filename": "Nama Berkas"
}
},
"split": {
"tags": "Pengoperasian halaman,membagi,Multi Halaman,memotong,sisi server",
@@ -634,7 +669,20 @@
"8": "Dokumen #6: Halaman 10"
},
"splitPages": "Masukkan halaman yang akan dipisah:",
"submit": "Pisahkan"
"submit": "Pisahkan",
"steps": {
"settings": "Pengaturan"
},
"methods": {
"bySize": {
"name": "Ukuran Berkas"
}
},
"value": {
"fileSize": {
"label": "Ukuran Berkas"
}
}
},
"rotate": {
"tags": "sisi server",
@@ -712,6 +760,12 @@
"type": {
"1": "Teks",
"2": "Gambar"
},
"watermarkType": {
"text": "Teks"
},
"settings": {
"fontSize": "Ukuran Fonta"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Ganti"
},
"removePages": {
"tags": "Menghapus halaman,menghapus halaman"
"tags": "Menghapus halaman,menghapus halaman",
"title": "Menghapus",
"submit": "Menghapus"
},
"addPassword": {
"tags": "aman,Keamanan",
@@ -758,7 +814,12 @@
"15": "Membatasi apa yang dapat dilakukan dengan dokumen setelah dibuka (Tidak didukung oleh semua pembaca)",
"16": "Membatasi pembukaan dokumen itu sendiri"
},
"submit": "Enkripsi"
"submit": "Enkripsi",
"tooltip": {
"permissions": {
"title": "Ganti Perizinan"
}
}
},
"removePassword": {
"tags": "aman,Dekripsi,keamanan,buka kata sandi,hapus kata sandi",
@@ -768,7 +829,12 @@
"1": "Pilih PDF yang akan di Dekrip",
"2": "Kata Sandi"
},
"submit": "Hapus"
"submit": "Hapus",
"desc": "Menghapus perlindungan kata sandi dari dokumen PDF Anda.",
"password": {
"stepTitle": "Hapus Kata Sandi",
"label": "Kata Sandi Saat Ini"
}
},
"compressPdfs": {
"tags": "remas, kecil, mini"
@@ -829,7 +895,25 @@
},
"help": "Silakan baca dokumentasi ini tentang cara menggunakan ini untuk bahasa lain dan/atau penggunaan yang tidak ada di docker",
"credit": "Layanan ini menggunakan qpdf dan Tesseract untuk OCR.",
"submit": "Memproses PDF dengan OCR"
"submit": "Memproses PDF dengan OCR",
"desc": "Memindai dan mendeteksi teks dari gambar di dalam PDF dan menambahkannya kembali sebagai teks.",
"settings": {
"title": "Pengaturan",
"ocrMode": {
"label": "Mode OCR"
},
"languages": {
"label": "Bahasa"
}
},
"tooltip": {
"mode": {
"title": "Mode OCR"
},
"languages": {
"title": "Bahasa"
}
}
},
"extractImages": {
"tags": "gambar, foto, simpan, arsip, zip, tangkap, ambil",
@@ -924,7 +1008,7 @@
"sharedSigs": "Tanda Tangan Berbagi",
"noSavedSigs": "Tidak ditemukan tanda tangan yang disimpan",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Hapus",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "Ratakan",
"header": "Ratakan PDF",
"flattenOnlyForms": "Ratakan hanya formulir",
"submit": "Ratakan"
"submit": "Ratakan",
"steps": {
"settings": "Pengaturan"
},
"options": {
"flattenOnlyForms": "Ratakan hanya formulir"
}
},
"repair": {
"tags": "perbaiki, pulihkan, koreksi, pulihkan",
@@ -1106,10 +1196,10 @@
"credit": "Menggunakan WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup, konten web, transformasi, konversi",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Konversi"
},
"getPdfInfo": {
"tags": "informasi, data, statistik, statistik",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,21 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Langkah Lanjut"
},
"wordsToRedact": {
"add": "Tambah"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Halaman-halaman",
"placeholder": "(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV, Ekstraksi Tabel, ekstrak, konversi"
@@ -1261,8 +1365,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Alasan",
"location": "Lokasi",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1385,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Versi",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1490,10 @@
"4": "Mode Otomatis - Menyesuaikan kualitas secara otomatis untuk mendapatkan PDF dengan ukuran yang tepat",
"5": "Ukuran PDF yang diharapkan (mis. 25MB, 10,8MB, 25KB)"
},
"submit": "Kompres"
"submit": "Kompres",
"method": {
"filesize": "Ukuran Berkas"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1664,74 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Unduh",
"convert": {
"title": "Konversi",
"settings": "Pengaturan",
"color": "Warna",
"greyscale": "Skala abu-abu",
"fillPage": "Isi Halaman",
"pdfaDigitalSignatureWarning": "PDF ini mengandung tanda tangan digital. Ini akan dihapus pada langkah berikutnya.",
"grayscale": "Skala abu-abu"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Tanda Tangan"
},
"fileUpload": {
"loading": "Mengambil data..."
},
"fileManager": {
"fileName": "Nama",
"fileVersion": "Versi",
"download": "Unduh",
"delete": "Hapus"
},
"sanitize": {
"submit": "Bersihkan PDF",
"steps": {
"settings": "Pengaturan"
}
},
"changePermissions": {
"title": "Ganti Perizinan",
"submit": "Ganti Perizinan",
"permissions": {
"preventAssembly": {
"label": "Pencegahan untuk penyusunan dokumen"
},
"preventExtractContent": {
"label": "Pencegahan untuk ekstraksi konten"
},
"preventExtractForAccessibility": {
"label": "Pencegahan ekstraksi untuk aksesibilitas"
},
"preventFillInForm": {
"label": "Pencegahan untuk mengisi formulir"
},
"preventModify": {
"label": "Pencegahan untuk pengubahan"
},
"preventModifyAnnotations": {
"label": "Pencegahan untuk perubahan anotasi"
},
"preventPrinting": {
"label": "Pencegahan untuk mencetak"
},
"preventPrintingFaithful": {
"label": "Pencegahan untuk mencetak format yang berbeda"
}
},
"tooltip": {
"header": {
"title": "Ganti Perizinan"
}
}
}
}
}
+202 -11
View File
@@ -578,6 +578,43 @@
"replace-color": {
"title": "Sostituisci e inverti il colore",
"desc": "Sostituisci il colore del testo e dello sfondo nel PDF e inverti il ​​colore completo del PDF per ridurre le dimensioni del file"
},
"convert": {
"title": "Converti"
},
"attachments": {
"title": "Aggiungi allegati",
"desc": "Aggiungere o rimuovere file incorporati (allegati) da/a un PDF"
},
"editTableOfContents": {
"title": "Modifica indice",
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF"
},
"extractPages": {
"title": "Estrai pagine"
},
"removePages": {
"title": "Rimuovi",
"desc": "Elimina alcune pagine dal PDF."
},
"removeImagePdf": {
"title": "Rimuovi immagine",
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file"
},
"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"
},
"adjust-contrast": {
"title": "Regola colori/contrasto",
"desc": "Regola contrasto, saturazione e luminosità di un PDF"
},
"replaceColorPdf": {
"title": "Sostituisci e inverti il colore",
"desc": "Sostituisci il colore del testo e dello sfondo nel PDF e inverti il ​​colore completo del PDF per ridurre le dimensioni del file"
},
"changePermissions": {
"title": "Cambia Permessi"
}
},
"viewPdf": {
@@ -617,7 +654,10 @@
"sortByName": "Ordina per nome",
"sortByDate": "Ordina per data",
"removeCertSign": "Rimuovere la firma digitale nel file unito?",
"submit": "Unisci"
"submit": "Unisci",
"sortBy": {
"filename": "Nome file"
}
},
"split": {
"tags": "Operazioni sulla pagina,divisione,multi pagina,taglio,lato server",
@@ -634,7 +674,20 @@
"8": "Documento #6: Pagine 10"
},
"splitPages": "Inserisci pagine a cui dividere:",
"submit": "Dividi"
"submit": "Dividi",
"steps": {
"settings": "Impostazioni"
},
"methods": {
"bySize": {
"name": "Dimensione"
}
},
"value": {
"fileSize": {
"label": "Dimensione"
}
}
},
"rotate": {
"tags": "lato server",
@@ -712,6 +765,12 @@
"type": {
"1": "Testo",
"2": "Immagine"
},
"watermarkType": {
"text": "Testo"
},
"settings": {
"fontSize": "Dimensione del font"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "Cambia Permessi"
},
"removePages": {
"tags": "Rimuovere pagine,eliminare pagine"
"tags": "Rimuovere pagine,eliminare pagine",
"title": "Rimuovi",
"submit": "Rimuovi"
},
"addPassword": {
"tags": "sicuro,sicurezza",
@@ -758,7 +819,12 @@
"15": "Limita le operazioni eseguibili con il documento una volta aperto (non supportato da tutti i lettori)",
"16": "Limita l'apertura del documento stesso"
},
"submit": "Crittografa"
"submit": "Crittografa",
"tooltip": {
"permissions": {
"title": "Cambia Permessi"
}
}
},
"removePassword": {
"tags": "Decriptare,proteggere,rimuovere la password,eliminare la password",
@@ -768,7 +834,12 @@
"1": "Seleziona PDF da decrittare",
"2": "Password"
},
"submit": "Rimuovi Password"
"submit": "Rimuovi Password",
"desc": "Rimuovi la password dal tuo PDF.",
"password": {
"stepTitle": "Rimuovi Password",
"label": "Password attuale"
}
},
"compressPdfs": {
"tags": "comprimere,piccolo,minuscolo"
@@ -829,7 +900,25 @@
},
"help": "Per favore leggi la documentazione su come usare il programma per altri linguaggi e/o uso non in Docker",
"credit": "Questo servizio utilizza Qpdf e Tesseract per l'OCR.",
"submit": "Scansiona testo nel PDF con OCR"
"submit": "Scansiona testo nel PDF con OCR",
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro.",
"settings": {
"title": "Impostazioni",
"ocrMode": {
"label": "Modalità OCR"
},
"languages": {
"label": "Lingue"
}
},
"tooltip": {
"mode": {
"title": "Modalità OCR"
},
"languages": {
"title": "Lingue"
}
}
},
"extractImages": {
"tags": "immagine,foto,salva,archivio,zip,catturare,prendere",
@@ -938,7 +1027,13 @@
"title": "Appiattire",
"header": "Appiattisci PDF",
"flattenOnlyForms": "Appiattisci solo i moduli",
"submit": "Appiattisci"
"submit": "Appiattisci",
"steps": {
"settings": "Impostazioni"
},
"options": {
"flattenOnlyForms": "Appiattisci solo i moduli"
}
},
"repair": {
"tags": "aggiustare,ripristinare,correggere,recuperare",
@@ -1174,7 +1269,22 @@
"showLayers": "Mostra livelli (fare doppio clic per ripristinare tutti i livelli allo stato predefinito)",
"colourPicker": "Selettore colore",
"findCurrentOutlineItem": "Trova l'elemento di contorno corrente",
"applyChanges": "Applica modifiche"
"applyChanges": "Applica modifiche",
"auto": {
"settings": {
"advancedTitle": "Avanzate"
},
"wordsToRedact": {
"add": "Aggiungi"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Pagine",
"placeholder": "(es. 1,2,8 o 4,7,12-16 o 2n-1)"
},
"export": "Esporta"
}
},
"tableExtraxt": {
"tags": "CSV,Estrazione tabella,estrai,converti"
@@ -1368,7 +1478,7 @@
"contrast": "Contrasto:",
"brightness": "Luminosità:",
"saturation": "Saturazione:",
"download": "Download"
"download": "Salva"
},
"compress": {
"title": "Comprimi",
@@ -1386,7 +1496,10 @@
"4": "Modalità automatica - Regola automaticamente la qualità per ottenere le dimensioni esatte del PDF",
"5": "Dimensioni PDF previste (ad es. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Comprimi"
"submit": "Comprimi",
"method": {
"filesize": "Dimensione"
}
},
"decrypt": {
"passwordPrompt": "Questo file è protetto da password. Inserisci la password:",
@@ -1557,5 +1670,83 @@
"description": "Questi cookie ci aiutano a capire come vengono utilizzati i nostri strumenti, così possiamo concentrarci sullo sviluppo delle funzionalità che la nostra community apprezza di più. Non preoccuparti: Stirling PDF non può e non traccerà mai il contenuto dei documenti con cui lavori."
}
}
},
"download": "Salva",
"undo": "Annulla",
"convert": {
"title": "Converti",
"settings": "Impostazioni",
"color": "Colore",
"greyscale": "Scala di grigi",
"fillPage": "Riempi la pagina",
"pdfaDigitalSignatureWarning": "Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo.",
"grayscale": "Scala di grigi"
},
"attachments": {
"tags": "incorporare,allegare,file,allegato,allegati",
"title": "Aggiungere allegati",
"header": "Aggiungi allegati",
"submit": "Aggiungi allegati"
},
"rightRail": {
"selectAll": "Seleziona tutto",
"deselectAll": "Deseleziona tutto"
},
"quickAccess": {
"sign": "Firma"
},
"fileUpload": {
"loading": "Caricamento...",
"or": "o"
},
"fileManager": {
"fileName": "Nome",
"fileVersion": "Versione",
"selectAll": "Seleziona tutto",
"deselectAll": "Deseleziona tutto",
"deleteSelected": "Elimina selezionata",
"download": "Salva",
"delete": "Elimina"
},
"sanitize": {
"submit": "Pulire PDF",
"steps": {
"settings": "Impostazioni"
}
},
"changePermissions": {
"title": "Cambia Permessi",
"submit": "Cambia Permessi",
"permissions": {
"preventAssembly": {
"label": "Previeni assemblaggio del documento"
},
"preventExtractContent": {
"label": "Previeni estrazione del contenuto"
},
"preventExtractForAccessibility": {
"label": "Previeni estrazione per accessibilità"
},
"preventFillInForm": {
"label": "Previeni compilazione dei moduli"
},
"preventModify": {
"label": "Previeni modifiche"
},
"preventModifyAnnotations": {
"label": "Previeni annotazioni"
},
"preventPrinting": {
"label": "Previeni stampa"
},
"preventPrintingFaithful": {
"label": "Previeni stampa in diversi formati"
}
},
"tooltip": {
"header": {
"title": "Cambia Permessi"
}
}
}
}
}
+210 -19
View File
@@ -118,14 +118,14 @@
"addToDoc": "ドキュメントに追加",
"reset": "リセット",
"apply": "適用",
"noFileSelected": "No file selected. Please upload one.",
"noFileSelected": "ファイルが選択されていません。アップロードしてください。",
"legal": {
"privacy": "プライバシーポリシー",
"terms": "利用規約",
"accessibility": "アクセシビリティ",
"cookie": "Cookieポリシー",
"impressum": "著作権利者情報",
"showCookieBanner": "Cookie Preferences"
"showCookieBanner": "クッキーの設定"
},
"pipeline": {
"header": "パイプラインメニュー (Beta)",
@@ -388,8 +388,8 @@
"desc": "PDFを圧縮してファイルサイズを小さくします。"
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "PDFフォームのロックを解除",
"desc": "PDFドキュメント内のフォームフィールドの読み取り専用プロパティを削除します。"
},
"changeMetadata": {
"title": "メタデータの変更",
@@ -578,6 +578,43 @@
"replace-color": {
"title": "色の置換と反転",
"desc": "PDF内のテキストと背景の色を置き換え、PDFのフルカラーを反転してファイルサイズを縮小します。"
},
"convert": {
"title": "変換"
},
"attachments": {
"title": "添付ファイルの追加",
"desc": "PDFに埋め込みファイル(添付ファイル)を追加または削除します"
},
"editTableOfContents": {
"title": "目次の編集",
"desc": "PDFドキュメントにしおりと目次を追加または編集します"
},
"extractPages": {
"title": "ページの抽出"
},
"removePages": {
"title": "削除",
"desc": "PDFから不要なページを削除します。"
},
"removeImagePdf": {
"title": "画像の削除",
"desc": "PDFから画像を削除してファイルサイズを小さくします"
},
"autoSizeSplitPDF": {
"title": "サイズ・数による自動分割",
"desc": "サイズ・ページ数またはドキュメント数に基づいて、1つのPDFを複数のドキュメントに分割します。"
},
"adjust-contrast": {
"title": "色/コントラストの調整",
"desc": "PDFのコントラスト、彩度、明るさを調整します。"
},
"replaceColorPdf": {
"title": "色の置換と反転",
"desc": "PDF内のテキストと背景の色を置き換え、PDFのフルカラーを反転してファイルサイズを縮小します。"
},
"changePermissions": {
"title": "権限の変更"
}
},
"viewPdf": {
@@ -617,7 +654,10 @@
"sortByName": "名前で並べ替え",
"sortByDate": "日付で並べ替え",
"removeCertSign": "結合されたファイル内のデジタル署名を削除しますか?",
"submit": "結合"
"submit": "結合",
"sortBy": {
"filename": "ファイル名"
}
},
"split": {
"tags": "Page operations,divide,Multi Page,cut,server side",
@@ -634,7 +674,20 @@
"8": "ドキュメント #6: ページ 10"
},
"splitPages": "分割するページ番号を入力:",
"submit": "分割"
"submit": "分割",
"steps": {
"settings": "設定"
},
"methods": {
"bySize": {
"name": "ファイルサイズ"
}
},
"value": {
"fileSize": {
"label": "ファイルサイズ"
}
}
},
"rotate": {
"tags": "server side",
@@ -644,10 +697,10 @@
"submit": "回転"
},
"imageToPdf": {
"tags": "conversion,img,jpg,picture,photo"
"tags": "conversion,img,jpg,picture,photo,psd,photoshop"
},
"pdfToImage": {
"tags": "conversion,img,jpg,picture,photo",
"tags": "conversion,img,jpg,picture,photo,psd,photoshop",
"title": "PDFを画像に変換",
"header": "PDFを画像に変換",
"selectText": "画像の形式",
@@ -712,6 +765,12 @@
"type": {
"1": "テキスト",
"2": "画像"
},
"watermarkType": {
"text": "テキスト"
},
"settings": {
"fontSize": "フォントサイズ"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "変更"
},
"removePages": {
"tags": "Remove pages,delete pages"
"tags": "Remove pages,delete pages",
"title": "削除",
"submit": "削除"
},
"addPassword": {
"tags": "secure,security",
@@ -758,7 +819,12 @@
"15": "ドキュメントを開いた後に実行できる操作を制限します (すべてのリーダーでサポートされているわけではありません)",
"16": "ドキュメントを開くことを制限します"
},
"submit": "暗号化"
"submit": "暗号化",
"tooltip": {
"permissions": {
"title": "権限の変更"
}
}
},
"removePassword": {
"tags": "secure,Decrypt,security,unpassword,delete password",
@@ -768,15 +834,20 @@
"1": "復号化するPDFを選択",
"2": "パスワード"
},
"submit": "削除"
"submit": "削除",
"desc": "PDFからパスワードの削除します。",
"password": {
"stepTitle": "パスワードの削除",
"label": "現在のパスワード"
}
},
"compressPdfs": {
"tags": "squish,small,tiny"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"title": "フォームフィールドから読み取り専用を削除",
"header": "PDFフォームのロックを解除",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +900,25 @@
},
"help": "他の言語でこれを使用する方法やDocker以外で使用する方法についてはこのドキュメントをお読みください。",
"credit": "本サービスにはOCRにqpdfとTesseractを使用しています。",
"submit": "OCRでPDFを処理する"
"submit": "OCRでPDFを処理する",
"desc": "クリーンアップはPDF内の画像からテキストを検出してテキストとして再追加します。",
"settings": {
"title": "設定",
"ocrMode": {
"label": "OCRモード"
},
"languages": {
"label": "言語"
}
},
"tooltip": {
"mode": {
"title": "OCRモード"
},
"languages": {
"title": "言語"
}
}
},
"extractImages": {
"tags": "picture,photo,save,archive,zip,capture,grab",
@@ -938,7 +1027,13 @@
"title": "平坦化",
"header": "PDFを平坦化する",
"flattenOnlyForms": "フォームのみを平坦にする",
"submit": "平坦化"
"submit": "平坦化",
"steps": {
"settings": "設定"
},
"options": {
"flattenOnlyForms": "フォームのみを平坦にする"
}
},
"repair": {
"tags": "fix,restore,correction,recover",
@@ -1174,7 +1269,22 @@
"showLayers": "レイヤーを表示(ダブルクリックするとすべてのレイヤーがデフォルトの状態にリセットされます)",
"colourPicker": "カラー選択",
"findCurrentOutlineItem": "現在のアウトライン項目を検索",
"applyChanges": "変更を適用"
"applyChanges": "変更を適用",
"auto": {
"settings": {
"advancedTitle": "アドバンスド"
},
"wordsToRedact": {
"add": "追加"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "ページ",
"placeholder": "(例:1,2,8、4,7,12-16、2n-1)"
},
"export": "エクスポート"
}
},
"tableExtraxt": {
"tags": "CSV,Table Extraction,extract,convert"
@@ -1336,7 +1446,7 @@
"alreadyLoggedIn": "すでにログインしています",
"alreadyLoggedIn2": "デバイスからログアウトしてもう一度お試しください。",
"toManySessions": "アクティブなセッションが多すぎます",
"logoutMessage": "You have been logged out."
"logoutMessage": "ログアウトしました"
},
"pdfToSinglePage": {
"title": "PDFを単一ページに変換",
@@ -1386,7 +1496,10 @@
"4": "自動モード - PDFを正確なサイズにするために品質を自動調整する。",
"5": "PDFサイズ (例:25MB, 10.8MB, 25KB)"
},
"submit": "圧縮"
"submit": "圧縮",
"method": {
"filesize": "ファイルサイズ"
}
},
"decrypt": {
"passwordPrompt": "このファイルはパスワードで保護されています。パスワードを入力してください:",
@@ -1557,5 +1670,83 @@
"description": "これらのCookieはツールがどのように使用されているかを把握するのに役立ちます。これによりコミュニティが最も重視する機能の開発に集中することができます。ご安心ください。Stirling PDFはお客様が操作するドキュメントの内容を追跡することは決してありません。"
}
}
},
"download": "ダウンロード",
"undo": "元に戻す",
"convert": {
"title": "変換",
"settings": "設定",
"color": "色",
"greyscale": "グレースケール",
"fillPage": "フルページ",
"pdfaDigitalSignatureWarning": "PDFにはデジタル署名が含まれています。これは次の手順で削除されます。",
"grayscale": "グレースケール"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "添付ファイルの追加",
"header": "添付ファイルの追加",
"submit": "添付ファイルの追加"
},
"rightRail": {
"selectAll": "すべて選択",
"deselectAll": "選択を解除"
},
"quickAccess": {
"sign": "署名"
},
"fileUpload": {
"loading": "読込中...",
"or": "または"
},
"fileManager": {
"fileName": "名前",
"fileVersion": "バージョン",
"selectAll": "すべて選択",
"deselectAll": "選択を解除",
"deleteSelected": "選択項目を削除",
"download": "ダウンロード",
"delete": "削除"
},
"sanitize": {
"submit": "PDFをサニタイズ",
"steps": {
"settings": "設定"
}
},
"changePermissions": {
"title": "権限の変更",
"submit": "権限の変更",
"permissions": {
"preventAssembly": {
"label": "ドキュメントの組立を禁止"
},
"preventExtractContent": {
"label": "コンテンツの抽出を禁止"
},
"preventExtractForAccessibility": {
"label": "アクセシビリティのための抽出を禁止"
},
"preventFillInForm": {
"label": "フォームへの入力を禁止"
},
"preventModify": {
"label": "変更を禁止"
},
"preventModifyAnnotations": {
"label": "注釈の変更を禁止"
},
"preventPrinting": {
"label": "印刷を禁止"
},
"preventPrintingFaithful": {
"label": "異なる形式の印刷を禁止"
}
},
"tooltip": {
"header": {
"title": "権限の変更"
}
}
}
}
}
+196 -11
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "로딩 중...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "색상 교체 및 반전",
"desc": "PDF에서 텍스트와 배경의 색상을 교체하고 파일 크기를 줄이기 위해 전체 PDF 색상을 반전"
},
"convert": {
"title": "변환"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "페이지 추출"
},
"removePages": {
"title": "제거",
"desc": "PDF 문서에서 원하지 않는 페이지를 삭제합니다."
},
"removeImagePdf": {
"title": "이미지 제거",
"desc": "파일 크기를 줄이기 위해 PDF에서 이미지 제거"
},
"autoSizeSplitPDF": {
"title": "크기/개수별 자동 분할",
"desc": "단일 PDF를 크기, 페이지 수 또는 문서 수를 기준으로 여러 문서로 분할"
},
"adjust-contrast": {
"title": "색상/대비 조정",
"desc": "PDF의 대비, 채도 및 밝기 조정"
},
"replaceColorPdf": {
"title": "색상 교체 및 반전",
"desc": "PDF에서 텍스트와 배경의 색상을 교체하고 파일 크기를 줄이기 위해 전체 PDF 색상을 반전"
},
"changePermissions": {
"title": "권한 변경"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "이름으로 정렬",
"sortByDate": "날짜로 정렬",
"removeCertSign": "병합된 파일에서 디지털 서명을 제거하시겠습니까?",
"submit": "병합"
"submit": "병합",
"sortBy": {
"filename": "파일 이름"
}
},
"split": {
"tags": "페이지 작업,나누기,멀티 페이지,자르기,서버 사이드",
@@ -634,7 +669,20 @@
"8": "문서 #6: 10페이지"
},
"splitPages": "분할할 페이지 입력:",
"submit": "분할"
"submit": "분할",
"steps": {
"settings": "설정"
},
"methods": {
"bySize": {
"name": "파일 크기"
}
},
"value": {
"fileSize": {
"label": "파일 크기"
}
}
},
"rotate": {
"tags": "서버 사이드",
@@ -712,6 +760,12 @@
"type": {
"1": "텍스트",
"2": "이미지"
},
"watermarkType": {
"text": "텍스트"
},
"settings": {
"fontSize": "글꼴 크기"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "변경"
},
"removePages": {
"tags": "페이지 제거,페이지 삭제"
"tags": "페이지 제거,페이지 삭제",
"title": "제거",
"submit": "제거"
},
"addPassword": {
"tags": "보안,보안",
@@ -758,7 +814,12 @@
"15": "문서가 열린 후 수행할 수 있는 작업 제한 (모든 리더에서 지원되지 않음)",
"16": "문서 자체 열기 제한"
},
"submit": "암호화"
"submit": "암호화",
"tooltip": {
"permissions": {
"title": "권한 변경"
}
}
},
"removePassword": {
"tags": "보안,암호 해제,보안,비밀번호 해제,비밀번호 삭제",
@@ -768,7 +829,12 @@
"1": "복호화할 PDF 선택",
"2": "비밀번호"
},
"submit": "제거"
"submit": "제거",
"desc": "PDF 문서에서 비밀번호 보호를 제거합니다.",
"password": {
"stepTitle": "비밀번호 제거",
"label": "현재 비밀번호"
}
},
"compressPdfs": {
"tags": "압축,작게,매우 작게"
@@ -829,7 +895,25 @@
},
"help": "다른 언어 사용 방법 및/또는 Docker에서 사용하지 않는 방법에 대한 문서를 읽어보세요",
"credit": "이 서비스는 OCR을 위해 qpdf와 Tesseract를 사용합니다.",
"submit": "OCR로 PDF 처리"
"submit": "OCR로 PDF 처리",
"desc": "스캔을 정리하고 PDF 내 이미지에서 텍스트를 감지하여 다시 텍스트로 추가합니다.",
"settings": {
"title": "설정",
"ocrMode": {
"label": "OCR 모드"
},
"languages": {
"label": "언어"
}
},
"tooltip": {
"mode": {
"title": "OCR 모드"
},
"languages": {
"title": "언어"
}
}
},
"extractImages": {
"tags": "사진,저장,아카이브,zip,캡처,가져오기",
@@ -938,7 +1022,13 @@
"title": "평면화",
"header": "PDF 평면화",
"flattenOnlyForms": "양식만 평면화",
"submit": "평면화"
"submit": "평면화",
"steps": {
"settings": "설정"
},
"options": {
"flattenOnlyForms": "양식만 평면화"
}
},
"repair": {
"tags": "수정,복원,교정,복구",
@@ -1174,7 +1264,22 @@
"showLayers": "레이어 보기 (더블클릭하여 모든 레이어를 기본 상태로 재설정)",
"colourPicker": "색상 선택기",
"findCurrentOutlineItem": "현재 개요 항목 찾기",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "고급"
},
"wordsToRedact": {
"add": "추가"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "페이지",
"placeholder": "(예: 1,2,8 또는 4,7,12-16 또는 2n-1)"
},
"export": "내보내기"
}
},
"tableExtraxt": {
"tags": "CSV,테이블-추출,추출,변환"
@@ -1386,7 +1491,10 @@
"4": "자동 모드 - PDF를 정확한 크기로 만들기 위해 품질 자동 조정",
"5": "예상 PDF 크기 (예: 25MB, 10.8MB, 25KB)"
},
"submit": "압축"
"submit": "압축",
"method": {
"filesize": "파일 크기"
}
},
"decrypt": {
"passwordPrompt": "이 파일은 비밀번호로 보호되어 있습니다. 비밀번호를 입력하세요:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "다운로드",
"convert": {
"title": "변환",
"settings": "설정",
"color": "색상",
"greyscale": "그레이스케일",
"fillPage": "페이지 채우기",
"pdfaDigitalSignatureWarning": "PDF에 디지털 서명이 포함되어 있습니다. 다음 단계에서 제거됩니다.",
"grayscale": "그레이스케일"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "모두 선택",
"deselectAll": "모두 선택 해제"
},
"quickAccess": {
"sign": "서명"
},
"fileUpload": {
"loading": "로딩 중...",
"or": "또는"
},
"fileManager": {
"fileName": "이름",
"fileVersion": "버전",
"selectAll": "모두 선택",
"deselectAll": "모두 선택 해제",
"deleteSelected": "선택 항목 삭제",
"download": "다운로드",
"delete": "삭제"
},
"sanitize": {
"submit": "PDF 정리",
"steps": {
"settings": "설정"
}
},
"changePermissions": {
"title": "권한 변경",
"submit": "권한 변경",
"permissions": {
"preventAssembly": {
"label": "문서 조립 방지"
},
"preventExtractContent": {
"label": "콘텐츠 추출 방지"
},
"preventExtractForAccessibility": {
"label": "접근성을 위한 추출 방지"
},
"preventFillInForm": {
"label": "양식 작성 방지"
},
"preventModify": {
"label": "수정 방지"
},
"preventModifyAnnotations": {
"label": "주석 수정 방지"
},
"preventPrinting": {
"label": "인쇄 방지"
},
"preventPrintingFaithful": {
"label": "다른 형식으로 인쇄 방지"
}
},
"tooltip": {
"header": {
"title": "권한 변경"
}
}
}
}
}
+197 -21
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Laden...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Vervang de kleur van tekst en achtergrond in een PDF en omverkeer de volledige kleur van het document om bestandsgrootte te verkleinen."
},
"convert": {
"title": "Omzetten"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Pagina's extraheren"
},
"removePages": {
"title": "Verwijderen",
"desc": "Verwijder ongewenste pagina's uit je PDF-document."
},
"removeImagePdf": {
"title": "Afbeelding verwijderen",
"desc": "Afbeeldingen uit PDF verwijderen om het bestandsgrootte te verminderen"
},
"autoSizeSplitPDF": {
"title": "Automatisch splitsen op grootte/aantal",
"desc": "Splits een enkele PDF in meerdere documenten op basis van grootte, aantal pagina's of aantal documenten"
},
"adjust-contrast": {
"title": "Kleuren/contrast aanpassen",
"desc": "Pas contrast, verzadiging en helderheid van een PDF aan"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Vervang de kleur van tekst en achtergrond in een PDF en omverkeer de volledige kleur van het document om bestandsgrootte te verkleinen."
},
"changePermissions": {
"title": "Rechten wijzigen"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "Pagina",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Splitsen",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Verwijderen",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sorteer op naam",
"sortByDate": "Sorteer op datum",
"removeCertSign": "Verwijder digitale handtekening in het samengevoegde bestand?",
"submit": "Samenvoegen"
"submit": "Samenvoegen",
"sortBy": {
"filename": "Bestandsnaam"
}
},
"split": {
"tags": "Pagina bewerkingen,verdelen,meerdere pagina's,knippen,serverzijde",
@@ -634,7 +669,20 @@
"8": "Document #6: Pagina 10"
},
"splitPages": "Voer pagina's in om op te splitsen:",
"submit": "Splitsen"
"submit": "Splitsen",
"steps": {
"settings": "Instellingen"
},
"methods": {
"bySize": {
"name": "Bestandsgrootte"
}
},
"value": {
"fileSize": {
"label": "Bestandsgrootte"
}
}
},
"rotate": {
"tags": "serverzijde",
@@ -712,6 +760,12 @@
"type": {
"1": "Tekst",
"2": "Afbeelding"
},
"watermarkType": {
"text": "Tekst"
},
"settings": {
"fontSize": "Lettertypegrootte"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Wijzigen"
},
"removePages": {
"tags": "Pagina's verwijderen"
"tags": "Pagina's verwijderen",
"title": "Verwijderen",
"submit": "Verwijderen"
},
"addPassword": {
"tags": "veilig,beveiliging",
@@ -758,7 +814,12 @@
"15": "Beperkt wat gedaan kan worden met het document nadat het is geopend (Niet ondersteund door alle lezers)",
"16": "Beperkt het openen van het document zelf"
},
"submit": "Versleutelen"
"submit": "Versleutelen",
"tooltip": {
"permissions": {
"title": "Rechten wijzigen"
}
}
},
"removePassword": {
"tags": "veilig,ontsleutelen,beveiliging,wachtwoord verwijderen",
@@ -768,7 +829,12 @@
"1": "Selecteer PDF om te decrypteren",
"2": "Wachtwoord"
},
"submit": "Verwijderen"
"submit": "Verwijderen",
"desc": "Verwijder wachtwoordbeveiliging van je PDF-document.",
"password": {
"stepTitle": "Wachtwoord verwijderen",
"label": "Huidige wachtwoord"
}
},
"compressPdfs": {
"tags": "comprimeren,klein"
@@ -829,7 +895,25 @@
},
"help": "Lees deze documentatie over hoe dit te gebruiken voor andere talen en/of gebruik buiten docker",
"credit": "Deze dienst maakt gebruik van qpdf en Tesseract voor OCR.",
"submit": "Verwerk PDF met OCR"
"submit": "Verwerk PDF met OCR",
"desc": "Ruim scans op, detecteert tekst van afbeeldingen in een PDF en voegt deze opnieuw toe als tekst.",
"settings": {
"title": "Instellingen",
"ocrMode": {
"label": "OCR-modus"
},
"languages": {
"label": "Talen"
}
},
"tooltip": {
"mode": {
"title": "OCR-modus"
},
"languages": {
"title": "Talen"
}
}
},
"extractImages": {
"tags": "foto,opslaan,archief,zip,vastleggen,plukken",
@@ -924,7 +1008,7 @@
"sharedSigs": "Gedeelde Signatuuren",
"noSavedSigs": "Geen opgeslagen signatuuren gevonden",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Verwijderen",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "Afvlakken",
"header": "PDF's afvlakken",
"flattenOnlyForms": "Alleen formulieren afvlakken",
"submit": "Afvlakken"
"submit": "Afvlakken",
"steps": {
"settings": "Instellingen"
},
"options": {
"flattenOnlyForms": "Alleen formulieren afvlakken"
}
},
"repair": {
"tags": "repareren,herstellen,correctie,terughalen",
@@ -1106,10 +1196,10 @@
"credit": "Gebruikt WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,web-inhoud,transformatie,omzetten",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Omzetten"
},
"getPdfInfo": {
"tags": "informatie,data,statistieken",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,21 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Geavanceerd"
},
"wordsToRedact": {
"add": "Toevoegen"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Pagen",
"placeholder": "(bijv. 1,2,8 of 4,7,12-16 of 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,tabel extractie,extractie,converteren"
@@ -1261,8 +1365,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Reden",
"location": "Locatie",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1385,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Versie",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1490,10 @@
"4": "Automatische modus - Past kwaliteit automatisch aan om PDF naar exacte grootte te krijgen",
"5": "Verwachte PDF-grootte (bijv. 25MB, 10.8MB, 25KB)"
},
"submit": "Comprimeren"
"submit": "Comprimeren",
"method": {
"filesize": "Bestandsgrootte"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1664,74 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Downloaden",
"convert": {
"title": "Omzetten",
"settings": "Instellingen",
"color": "Kleur",
"greyscale": "Grijstinten",
"fillPage": "Pagina vullen",
"pdfaDigitalSignatureWarning": "Dit PDF bestand bevat een digitale handtekening. Deze wordt in de volgende stap verwijderd.",
"grayscale": "Grijstinten"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Ondertekenen"
},
"fileUpload": {
"loading": "Laden..."
},
"fileManager": {
"fileName": "Naam",
"fileVersion": "Versie",
"download": "Downloaden",
"delete": "Verwijderen"
},
"sanitize": {
"submit": "PDF opschonen",
"steps": {
"settings": "Instellingen"
}
},
"changePermissions": {
"title": "Rechten wijzigen",
"submit": "Rechten wijzigen",
"permissions": {
"preventAssembly": {
"label": "Voorkom samenvoegen van document"
},
"preventExtractContent": {
"label": "Voorkom inhoudsextractie"
},
"preventExtractForAccessibility": {
"label": "Voorkom extractie voor toegankelijkheid"
},
"preventFillInForm": {
"label": "Voorkom invullen van formulier"
},
"preventModify": {
"label": "Voorkom wijziging"
},
"preventModifyAnnotations": {
"label": "Voorkom annotatie wijziging"
},
"preventPrinting": {
"label": "Voorkom afdrukken"
},
"preventPrintingFaithful": {
"label": "Voorkom afdrukken in verschillende formaten"
}
},
"tooltip": {
"header": {
"title": "Rechten wijzigen"
}
}
}
}
}
+195 -18
View File
@@ -137,7 +137,7 @@
"scanHelp": "Mappe skanning hjelp",
"deletePrompt": "Er du sikker på at du vil slette denne pipelinen?",
"tags": "automatisere,sekvens,skriptet,batch-prosess",
"title": "Pipeline"
"title": "Pipeline (Avansert)"
},
"pipelineOptions": {
"header": "Pipeline konfigurasjon",
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Laster...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Erstatt og Inverter Farge",
"desc": "Erstatt farge for tekst og bakgrunn i PDF og inverter full farge av pdf for å redusere filstørrelsen"
},
"convert": {
"title": "Konverter"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Trekk ut Sider"
},
"removePages": {
"title": "Fjern",
"desc": "Slett uønskede sider fra din PDF-dokument."
},
"removeImagePdf": {
"title": "Fjern bilde",
"desc": "Fjern bilde fra PDF for å redusere filstørrelsen"
},
"autoSizeSplitPDF": {
"title": "Auto Del etter Størrelse/Antall",
"desc": "Del en enkelt PDF i flere dokumenter basert på størrelse, antall sider eller dokumenter"
},
"adjust-contrast": {
"title": "Juster Farger/Kontrast",
"desc": "Juster kontrast, metning og lysstyrke i en PDF"
},
"replaceColorPdf": {
"title": "Erstatt og Inverter Farge",
"desc": "Erstatt farge for tekst og bakgrunn i PDF og inverter full farge av pdf for å redusere filstørrelsen"
},
"changePermissions": {
"title": "Endre tillatelser"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "Side",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Del opp",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Slett",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sorter etter navn",
"sortByDate": "Sorter etter dato",
"removeCertSign": "Fjern digital signatur i den sammenslåtte filen?",
"submit": "Slå sammen"
"submit": "Slå sammen",
"sortBy": {
"filename": "Fil navn"
}
},
"split": {
"tags": "sideoperasjoner,del,flersidig,kutt,serverside",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Side 10"
},
"splitPages": "Skriv inn sidene som skal deles på:",
"submit": "Del"
"submit": "Del",
"steps": {
"settings": "Innstillinger"
},
"methods": {
"bySize": {
"name": "Filstørrelse"
}
},
"value": {
"fileSize": {
"label": "Filstørrelse"
}
}
},
"rotate": {
"tags": "serverside",
@@ -712,6 +760,12 @@
"type": {
"1": "Tekst",
"2": "Bilde"
},
"watermarkType": {
"text": "Tekst"
},
"settings": {
"fontSize": "Skriftstørrelse"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Endre"
},
"removePages": {
"tags": "fjern sider,slett sider"
"tags": "fjern sider,slett sider",
"title": "Fjern",
"submit": "Fjern"
},
"addPassword": {
"tags": "sikker,trygghet",
@@ -758,7 +814,12 @@
"15": "Begrenser hva som kan gjøres med dokumentet når det er åpnet (Støttes ikke av alle leserprogrammer)",
"16": "Begrenser åpningen av dokumentet selv"
},
"submit": "Krypter"
"submit": "Krypter",
"tooltip": {
"permissions": {
"title": "Endre tillatelser"
}
}
},
"removePassword": {
"tags": "sikker,dekrypter,trygghet,upassord,slett passord",
@@ -768,7 +829,12 @@
"1": "Velg PDF for å dekryptere",
"2": "Passord"
},
"submit": "Fjern"
"submit": "Fjern",
"desc": "Fjern passordbeskyttelse fra din PDF-dokument.",
"password": {
"stepTitle": "Fjern Passord",
"label": "Nåværende Passord"
}
},
"compressPdfs": {
"tags": "komprimer,liten,tiny"
@@ -829,7 +895,25 @@
},
"help": "Vennligst les denne dokumentasjonen for hvordan du bruker dette for andre språk og/eller bruk utenfor Docker.",
"credit": "Denne tjenesten bruker qpdf og Tesseract for OCR.",
"submit": "Behandle PDF med OCR"
"submit": "Behandle PDF med OCR",
"desc": "Rydd opp skanninger og oppdag tekst fra bilder i en PDF og legg den til som tekst.",
"settings": {
"title": "Innstillinger",
"ocrMode": {
"label": "OCR-modus"
},
"languages": {
"label": "Språk"
}
},
"tooltip": {
"mode": {
"title": "OCR-modus"
},
"languages": {
"title": "Språk"
}
}
},
"extractImages": {
"tags": "bilde,foto,lagre,arkiv,zip,fangst,hent",
@@ -938,7 +1022,13 @@
"title": "Utjevning",
"header": "Utjevning av PDf",
"flattenOnlyForms": "Utjevning av kun skjemaer",
"submit": "Utjevn"
"submit": "Utjevn",
"steps": {
"settings": "Innstillinger"
},
"options": {
"flattenOnlyForms": "Utjevning av kun skjemaer"
}
},
"repair": {
"tags": "fiks,gjenopprett,korreksjon,gjenoppretting",
@@ -1106,10 +1196,10 @@
"credit": "Bruker WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,web-innhold,transformasjon,konverter",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Konverter"
},
"getPdfInfo": {
"tags": "informasjon,data,statistikk",
@@ -1174,7 +1264,21 @@
"showLayers": "Vis lag (dobbeltklikk for å tilbakestille alle lag til standardtilstand)",
"colourPicker": "Fargevelger",
"findCurrentOutlineItem": "Finn gjeldende punkt i strukturen",
"applyChanges": "Bruk endringer"
"applyChanges": "Bruk endringer",
"auto": {
"settings": {
"advancedTitle": "Avansert"
},
"wordsToRedact": {
"add": "Legg til"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Sider",
"placeholder": "(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,tabelluttrekk,ekstrahere,konvertere"
@@ -1386,7 +1490,10 @@
"4": "Automatisk modus - Justerer automatisk kvaliteten for å få PDF til nøyaktig størrelse",
"5": "Forventet PDF-størrelse (f.eks. 25MB, 10.8MB, 25KB)"
},
"submit": "Komprimer"
"submit": "Komprimer",
"method": {
"filesize": "Filstørrelse"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1501,7 +1608,7 @@
"3": "Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.",
"4": "Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs."
},
"submit": "Split PDF"
"submit": "Del PDF"
},
"fileChooser": {
"click": "Click",
@@ -1557,5 +1664,75 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Last ned",
"undo": "Angre",
"convert": {
"title": "Konverter",
"settings": "Innstillinger",
"color": "Farge",
"greyscale": "Gråtone",
"fillPage": "Fyll side",
"pdfaDigitalSignatureWarning": "PDFen inneholder en digital signatur. Denne vil bli fjernet i neste steg.",
"grayscale": "Gråtone"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Signer"
},
"fileUpload": {
"loading": "Laster..."
},
"fileManager": {
"fileName": "Navn",
"fileVersion": "Versjon",
"download": "Last ned",
"delete": "Slett"
},
"sanitize": {
"submit": "Rensker PDF",
"steps": {
"settings": "Innstillinger"
}
},
"changePermissions": {
"title": "Endre tillatelser",
"submit": "Endre tillatelser",
"permissions": {
"preventAssembly": {
"label": "Forhindre sammenstilling av dokumentet"
},
"preventExtractContent": {
"label": "Forhindre innholdsekstraksjon"
},
"preventExtractForAccessibility": {
"label": "Forhindre ekstraksjon for tilgjengelighet"
},
"preventFillInForm": {
"label": "Forhindre utfylling av skjema"
},
"preventModify": {
"label": "Forhindre modifisering"
},
"preventModifyAnnotations": {
"label": "Forhindre annotasjonsmodifisering"
},
"preventPrinting": {
"label": "Forhindre utskrift"
},
"preventPrintingFaithful": {
"label": "Forhindre utskrift i ulike formater"
}
},
"tooltip": {
"header": {
"title": "Endre tillatelser"
}
}
}
}
}
+197 -11
View File
@@ -320,7 +320,7 @@
"desc": "Twoja lokalna aplikacja do kompleksowej obsługi Twoich potrzeb związanych z dokumentami PDF.",
"searchBar": "Szukaj opcji ...",
"viewPdf": {
"title": "View/Edit PDF",
"title": "Przeglądaj/Edytuj PDF",
"desc": "Wyświetl, adnotuj, dodaj tekst lub obrazy"
},
"setFavorites": "Ustaw ulubione",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Zastąp i Odwróć Kolor",
"desc": "Zastąp kolor tekstu i tła w pliku PDF i odwróć pełen kolor pliku PDF, aby zmniejszyć rozmiar pliku"
},
"convert": {
"title": "Konwertuj"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Wyciągnij stronę"
},
"removePages": {
"title": "Usuń",
"desc": "Usuń niechciane strony z dokumentu PDF."
},
"removeImagePdf": {
"title": "Usuń obraz",
"desc": "Usuń obraz z pliku PDF, aby zmniejszyć rozmiar pliku"
},
"autoSizeSplitPDF": {
"title": "Podziel (Rozmiar/Ilość stron)",
"desc": "Rozdziela dokument PDF na wiele dokumentów bazując na podanym rozmiarze, ilości stron bądź ilości dokumentów"
},
"adjust-contrast": {
"title": "Zmień kolor/nasycenie/jasność",
"desc": "Zmień kolor/nasycenie/jasność w dokumencie PDF"
},
"replaceColorPdf": {
"title": "Zastąp i Odwróć Kolor",
"desc": "Zastąp kolor tekstu i tła w pliku PDF i odwróć pełen kolor pliku PDF, aby zmniejszyć rozmiar pliku"
},
"changePermissions": {
"title": "Zmień uprawnienia"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Sortuj po nazwie",
"sortByDate": "Sortuj po dacie",
"removeCertSign": "Usuń podpis cyfrowy w scalonym pliku?",
"submit": "Połącz"
"submit": "Połącz",
"sortBy": {
"filename": "Nazwa pliku"
}
},
"split": {
"tags": "Operacje na stronach, dzielenie, wiele stron, cięcie, po stronie serwera",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Strona 10"
},
"splitPages": "Wprowadź strony do podziału na:",
"submit": "Podziel"
"submit": "Podziel",
"steps": {
"settings": "Ustawienia"
},
"methods": {
"bySize": {
"name": "Rozmiar pliku"
}
},
"value": {
"fileSize": {
"label": "Rozmiar pliku"
}
}
},
"rotate": {
"tags": "strona serwera",
@@ -712,6 +760,12 @@
"type": {
"1": "Tekst",
"2": "Obraz"
},
"watermarkType": {
"text": "Tekst"
},
"settings": {
"fontSize": "Rozmiar Czcionki"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Zmień"
},
"removePages": {
"tags": "Usuń strony,usuwaj strony"
"tags": "Usuń strony,usuwaj strony",
"title": "Usuń",
"submit": "Usuń"
},
"addPassword": {
"tags": "bezpieczeństwo,ochrona",
@@ -758,7 +814,12 @@
"15": "Ogranicza akcje, które można wykonać na dokumencie, kiedy jest otwarty (nie wspierany przez wszystkie przeglądarki)",
"16": "Ogranicza otwarcie dokumentu"
},
"submit": "Zablokuj"
"submit": "Zablokuj",
"tooltip": {
"permissions": {
"title": "Zmień uprawnienia"
}
}
},
"removePassword": {
"tags": "zabezpieczenie,odszyfrowanie,bezpieczeństwo,odhasłowanie,usunięcie hasła",
@@ -768,7 +829,12 @@
"1": "Wybierz dokument PDF do odszyfrowania",
"2": "Hasło"
},
"submit": "Usuń"
"submit": "Usuń",
"desc": "Usuń ochronę hasłem z dokumentu PDF.",
"password": {
"stepTitle": "Usuń hasło",
"label": "Obecne hasło"
}
},
"compressPdfs": {
"tags": "zgniatać,mały,malutki"
@@ -829,7 +895,25 @@
},
"help": "Przeczytaj tę dokumentację, aby dowiedzieć się, jak używać tego w innych językach i/lub nie używać docker",
"credit": "Ta usługa używa qpdf i Tesseract do OCR.",
"submit": "Przetwarzaj PDF za pomocą OCR"
"submit": "Przetwarzaj PDF za pomocą OCR",
"desc": "OCR skanuje i wykrywa tekst z obrazów w dokumencie PDF i zamienia go na tekst.",
"settings": {
"title": "Ustawienia",
"ocrMode": {
"label": "Tryb OCR"
},
"languages": {
"label": "Języki"
}
},
"tooltip": {
"mode": {
"title": "Tryb OCR"
},
"languages": {
"title": "Języki"
}
}
},
"extractImages": {
"tags": "obraz, zdjęcie, zapisz, archiwum, zip, przechwyć, złap",
@@ -938,7 +1022,13 @@
"title": "Spłaszcz",
"header": "Spłaszcz dokument(y) PDF",
"flattenOnlyForms": "Spłaszcz tylko formularze",
"submit": "Spłaszcz"
"submit": "Spłaszcz",
"steps": {
"settings": "Ustawienia"
},
"options": {
"flattenOnlyForms": "Spłaszcz tylko formularze"
}
},
"repair": {
"tags": "naprawianie, naprawa, przywracanie, poprawianie, odzyskiwanie",
@@ -1174,7 +1264,22 @@
"showLayers": "Pokaż warstwy (kliknij dwukrotnie, aby przywrócić domyślny stan warstw)",
"colourPicker": "Selektor kolorów",
"findCurrentOutlineItem": "Znajdź bieżący element zarysu",
"applyChanges": "Zastosuj zmiany"
"applyChanges": "Zastosuj zmiany",
"auto": {
"settings": {
"advancedTitle": "Zaawansowane"
},
"wordsToRedact": {
"add": "Dodaj"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Strony",
"placeholder": "(przykład 1,2,8 lub 2n-1)"
},
"export": "Eksportuj"
}
},
"tableExtraxt": {
"tags": "CSV, ekstrakcja tabeli, ekstrakcja, konwersja, wydobywanie"
@@ -1386,7 +1491,10 @@
"4": "Tryb automatyczny - Automatycznie dostosowuje jakość, aby uzyskać dokładny rozmiar pliku PDF",
"5": "Oczekiwany rozmiar pliku PDF (np. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Kompresuj"
"submit": "Kompresuj",
"method": {
"filesize": "Rozmiar pliku"
}
},
"decrypt": {
"passwordPrompt": "Ten plik jest chroniony hasłem. Wprowadź hasło:",
@@ -1557,5 +1665,83 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Pobierz",
"undo": "Cofnij",
"convert": {
"title": "Konwertuj",
"settings": "Ustawienia",
"color": "kolor",
"greyscale": "Odcień szarości",
"fillPage": "Wypełnij stronę",
"pdfaDigitalSignatureWarning": "Dokument zawiera podpis cyfrowy, nie zostanie on wczytany.",
"grayscale": "Odcień szarości"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Zaznacz wszystko",
"deselectAll": "Odznacz wszystko"
},
"quickAccess": {
"sign": "Podpis"
},
"fileUpload": {
"loading": "Ładowanie...",
"or": "lub"
},
"fileManager": {
"fileName": "Nazwa",
"fileVersion": "Wersja",
"selectAll": "Zaznacz wszystko",
"deselectAll": "Odznacz wszystko",
"deleteSelected": "Usuń zaznaczone",
"download": "Pobierz",
"delete": "usuń"
},
"sanitize": {
"submit": "Dezynfekuj PDF",
"steps": {
"settings": "Ustawienia"
}
},
"changePermissions": {
"title": "Zmień uprawnienia",
"submit": "Zmień uprawnienia",
"permissions": {
"preventAssembly": {
"label": "Zablokuj zmiany w dokumencie"
},
"preventExtractContent": {
"label": "Zablokuj zmiany w treści"
},
"preventExtractForAccessibility": {
"label": "Zablokuj zmiany w celu ułatwienia dostępu"
},
"preventFillInForm": {
"label": "Zablokuj wypełnianie formularzy"
},
"preventModify": {
"label": "Zablokuj modyfikacje"
},
"preventModifyAnnotations": {
"label": "Zablokuj modyfikacje adnotacji"
},
"preventPrinting": {
"label": "Zablokuj drukowanie"
},
"preventPrintingFaithful": {
"label": "Zablokuj drukowanie różnych formatów"
}
},
"tooltip": {
"header": {
"title": "Zmień uprawnienia"
}
}
}
}
}
+202 -16
View File
@@ -388,8 +388,8 @@
"desc": "Comprimir PDFs para reduzir o tamanho do arquivo."
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "Desbloquear Formulários do PDF",
"desc": "Remove a propriedade de apenas leitura de formulários em um documento PDF."
},
"changeMetadata": {
"title": "Alterar Metadados",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Substitui e Inverte Cores",
"desc": "Substitui cor do texto e plano de fundo de um PDF e/ou inverte a toda cor do PDF para reduzir o tamanho."
},
"convert": {
"title": "Converter"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrair Página(s)"
},
"removePages": {
"title": "Remover Páginas",
"desc": "Excluir páginas indesejadas do seu PDF."
},
"removeImagePdf": {
"title": "Remover Imagem",
"desc": "Remova imagens do PDF para reduzir o tamanho do arquivo."
},
"autoSizeSplitPDF": {
"title": "Divisão Manual do PDF",
"desc": "Divida um PDF em vários, com base no tamanho, contagem de páginas ou contagem de documentos."
},
"adjust-contrast": {
"title": "Ajuste Visual do PDF",
"desc": "Ajustar Contraste, Saturação e Brilho de um PDF."
},
"replaceColorPdf": {
"title": "Substitui e Inverte Cores",
"desc": "Substitui cor do texto e plano de fundo de um PDF e/ou inverte a toda cor do PDF para reduzir o tamanho."
},
"changePermissions": {
"title": "Alterar Permissões"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Classificar por Nome",
"sortByDate": "Classificar por Data",
"removeCertSign": "Remover a assinatura digital do arquivo mesclado?",
"submit": "Mesclar"
"submit": "Mesclar",
"sortBy": {
"filename": "Nome do Arquivo"
}
},
"split": {
"tags": "Operações de Página,dividir,Múltiplas Páginas,cortar,lado do servidor",
@@ -634,7 +669,20 @@
"8": "Documento Nº6: Página 10 e 11 (e mais se tiver)"
},
"splitPages": "Digite as páginas para a divisão:",
"submit": "Dividir"
"submit": "Dividir",
"steps": {
"settings": "Configurações"
},
"methods": {
"bySize": {
"name": "Tamanho do Arquivo"
}
},
"value": {
"fileSize": {
"label": "Tamanho do Arquivo"
}
}
},
"rotate": {
"tags": "Lado do servidor",
@@ -712,6 +760,12 @@
"type": {
"1": "Texto",
"2": "Imagem"
},
"watermarkType": {
"text": "Texto"
},
"settings": {
"fontSize": "Tamanho da Fonte"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Alterar"
},
"removePages": {
"tags": "Remover páginas,excluir páginas"
"tags": "Remover páginas,excluir páginas",
"title": "Remover Páginas",
"submit": "Remover Páginas"
},
"addPassword": {
"tags": "seguro,segurança",
@@ -758,7 +814,12 @@
"15": "Restringe o que pode ser feito com o documento depois de aberto (não suportado por todos os leitores).",
"16": "Restringe a abertura do próprio documento."
},
"submit": "Criptografar"
"submit": "Criptografar",
"tooltip": {
"permissions": {
"title": "Alterar Permissões"
}
}
},
"removePassword": {
"tags": "seguro, descriptografar, segurança, remover senha",
@@ -768,15 +829,20 @@
"1": "Selecione o PDF para descriptografar:",
"2": "Senha"
},
"submit": "Descriptografar"
"submit": "Descriptografar",
"desc": "Descriptografar o PDF realizando a remoção da senha.",
"password": {
"stepTitle": "Desproteger PDF",
"label": "Senha Atual"
}
},
"compressPdfs": {
"tags": "compactar,pequeno,mínimo"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"tags": "remoção,apagar,formulario,campo,apenasleitura",
"title": "Remover Apenas Leitura de Formulários",
"header": "Desbloquear Formulários do PDF",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +895,25 @@
},
"help": "Por favor, leia a documentação abaixo para saber mais sobre OCR e sua utilização.",
"credit": "Este serviço usa Qpdf e Tesseract para OCR.",
"submit": "Processar OCR no PDF"
"submit": "Processar OCR no PDF",
"desc": "Reconhecimento Óptico de Caracteres transforma PDFs com imagens em documentos pesquisáveis e com texto selecionável.",
"settings": {
"title": "Configurações",
"ocrMode": {
"label": "Modo OCR:"
},
"languages": {
"label": "Idiomas"
}
},
"tooltip": {
"mode": {
"title": "Modo OCR:"
},
"languages": {
"title": "Idiomas"
}
}
},
"extractImages": {
"tags": "imagem,foto,salvar,arquivo,zip,captura,coleta",
@@ -938,7 +1022,13 @@
"title": "Achatar",
"header": "Achatar",
"flattenOnlyForms": "Achatar apenas formulários interativos.",
"submit": "Achatar"
"submit": "Achatar",
"steps": {
"settings": "Configurações"
},
"options": {
"flattenOnlyForms": "Achatar apenas formulários interativos."
}
},
"repair": {
"tags": "corrigir,restaurar,correção,recuperar",
@@ -1174,7 +1264,22 @@
"showLayers": "Mostrar Camadas (duplo clique para restabelecer as camadas para o estado padrão)",
"colourPicker": "Seletor de Cores",
"findCurrentOutlineItem": "Encontrar item atual",
"applyChanges": "Aplicar Alterações"
"applyChanges": "Aplicar Alterações",
"auto": {
"settings": {
"advancedTitle": "Avançado"
},
"wordsToRedact": {
"add": "Adicionar"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Páginas",
"placeholder": "(por exemplo 1,2,8 ou 4,7,12-16 ou 2n-1)"
},
"export": "Exportar"
}
},
"tableExtraxt": {
"tags": "CSV,extração de tabela,extrair,converter"
@@ -1336,7 +1441,7 @@
"alreadyLoggedIn": "Você já está conectado em",
"alreadyLoggedIn2": "aparelhos. Por favor saia dos aparelhos e tente novamente.",
"toManySessions": "Você tem muitas sessões ativas",
"logoutMessage": "You have been logged out."
"logoutMessage": "Você foi desconectado."
},
"pdfToSinglePage": {
"title": "PDF para Página Única",
@@ -1386,7 +1491,10 @@
"4": "Modo Automático - Ajusta automaticamente a qualidade para atingir o tamanho exato desejado",
"5": "Tamanho esperado do PDF (por exemplo, 25 MB, 10,8 MB, 25 KB):"
},
"submit": "Comprimir"
"submit": "Comprimir",
"method": {
"filesize": "Tamanho do Arquivo"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1665,83 @@
"description": "Estes cookies nos ajudam a entender como nossas ferramentas estão sendo utilizadas, para que possamos nos concentrar na construção dos recursos que nossa comunidade mais valoriza. Fique tranquilo: o Stirling PDF não pode e nunca rastreará o conteúdo dos documentos com os quais você manipula."
}
}
},
"download": "Baixar (JSON)",
"undo": "Desfazer",
"convert": {
"title": "Converter",
"settings": "Configurações",
"color": "Cor",
"greyscale": "Escala de Cinza",
"fillPage": "Preencher a página",
"pdfaDigitalSignatureWarning": "O PDF contém uma assinatura digital. Isso será removido na próxima etapa.",
"grayscale": "Escala de Cinza"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Selecionar Tudo",
"deselectAll": "Desselecionar Tudo"
},
"quickAccess": {
"sign": "Assinar"
},
"fileUpload": {
"loading": "Carregando...",
"or": "ou"
},
"fileManager": {
"fileName": "Nome",
"fileVersion": "Versão",
"selectAll": "Selecionar Tudo",
"deselectAll": "Desselecionar Tudo",
"deleteSelected": "Apagar Selecionados",
"download": "Baixar (JSON)",
"delete": "Apagar"
},
"sanitize": {
"submit": "Higienizar",
"steps": {
"settings": "Configurações"
}
},
"changePermissions": {
"title": "Alterar Permissões",
"submit": "Alterar Permissões",
"permissions": {
"preventAssembly": {
"label": "Impedir montagem do documento."
},
"preventExtractContent": {
"label": "Impedir extração de conteúdo."
},
"preventExtractForAccessibility": {
"label": "Impedir extração para acessibilidade."
},
"preventFillInForm": {
"label": "Impedir preenchimento de formulário."
},
"preventModify": {
"label": "Impedir modificações."
},
"preventModifyAnnotations": {
"label": "Impedir modificação de anotações."
},
"preventPrinting": {
"label": "Impedir impressão."
},
"preventPrintingFaithful": {
"label": "Impedir impressão de formatos diferentes."
}
},
"tooltip": {
"header": {
"title": "Alterar Permissões"
}
}
}
}
}
+198 -13
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "A carregar...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Substituir e Inverter Cor",
"desc": "Substituir cor para texto e fundo em PDF e inverter cor completa do pdf para reduzir tamanho do ficheiro"
},
"convert": {
"title": "Converter"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrair Páginas"
},
"removePages": {
"title": "Remover",
"desc": "Eliminar páginas indesejadas do seu documento PDF."
},
"removeImagePdf": {
"title": "Remover imagem",
"desc": "Remover imagem do PDF para reduzir tamanho do ficheiro"
},
"autoSizeSplitPDF": {
"title": "Divisão Automática por Tamanho/Contagem",
"desc": "Dividir um único PDF em múltiplos documentos baseado em tamanho, contagem de páginas, ou contagem de documentos"
},
"adjust-contrast": {
"title": "Ajustar Cores/Contraste",
"desc": "Ajustar Contraste, Saturação e Brilho de um PDF"
},
"replaceColorPdf": {
"title": "Substituir e Inverter Cor",
"desc": "Substituir cor para texto e fundo em PDF e inverter cor completa do pdf para reduzir tamanho do ficheiro"
},
"changePermissions": {
"title": "Alterar Permissões"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Ordenar por nome",
"sortByDate": "Ordenar por data",
"removeCertSign": "Remover assinatura digital no ficheiro junto?",
"submit": "Juntar"
"submit": "Juntar",
"sortBy": {
"filename": "Nome do Ficheiro"
}
},
"split": {
"tags": "Operações de página,dividir,Multi Página,cortar,lado servidor",
@@ -634,7 +669,20 @@
"8": "Documento #6: Página 10"
},
"splitPages": "Introduza páginas para dividir:",
"submit": "Dividir"
"submit": "Dividir",
"steps": {
"settings": "Definições"
},
"methods": {
"bySize": {
"name": "Tamanho do Ficheiro"
}
},
"value": {
"fileSize": {
"label": "Tamanho do Ficheiro"
}
}
},
"rotate": {
"tags": "lado servidor",
@@ -712,6 +760,12 @@
"type": {
"1": "Texto",
"2": "Imagem"
},
"watermarkType": {
"text": "Texto"
},
"settings": {
"fontSize": "Tamanho da Fonte"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Alterar"
},
"removePages": {
"tags": "Remover páginas,eliminar páginas"
"tags": "Remover páginas,eliminar páginas",
"title": "Remover",
"submit": "Remover"
},
"addPassword": {
"tags": "seguro,segurança",
@@ -758,7 +814,12 @@
"15": "Restringe o que pode ser feito com o documento uma vez aberto (Não suportado por todos os leitores)",
"16": "Restringe a abertura do próprio documento"
},
"submit": "Encriptar"
"submit": "Encriptar",
"tooltip": {
"permissions": {
"title": "Alterar Permissões"
}
}
},
"removePassword": {
"tags": "seguro,Desencriptar,segurança,sem palavra-passe,eliminar palavra-passe",
@@ -768,7 +829,12 @@
"1": "Selecione PDF para Desencriptar",
"2": "Palavra-passe"
},
"submit": "Remover"
"submit": "Remover",
"desc": "Remover proteção por palavra-passe do seu documento PDF.",
"password": {
"stepTitle": "Remover Palavra-passe",
"label": "Palavra-passe Atual"
}
},
"compressPdfs": {
"tags": "comprimir,pequeno,minúsculo"
@@ -829,7 +895,25 @@
},
"help": "Por favor leia esta documentação sobre como usar isto para outros idiomas e/ou usar fora do docker",
"credit": "Este serviço usa qpdf e Tesseract para OCR.",
"submit": "Processar PDF com OCR"
"submit": "Processar PDF com OCR",
"desc": "Limpa digitalizações e deteta texto de imagens dentro de um PDF e readiciona-o como texto.",
"settings": {
"title": "Definições",
"ocrMode": {
"label": "Modo OCR"
},
"languages": {
"label": "Idiomas"
}
},
"tooltip": {
"mode": {
"title": "Modo OCR"
},
"languages": {
"title": "Idiomas"
}
}
},
"extractImages": {
"tags": "imagem,foto,guardar,arquivo,zip,capturar,extrair",
@@ -938,7 +1022,13 @@
"title": "Achatar",
"header": "Achatar PDFs",
"flattenOnlyForms": "Achatar apenas formulários",
"submit": "Achatar"
"submit": "Achatar",
"steps": {
"settings": "Definições"
},
"options": {
"flattenOnlyForms": "Achatar apenas formulários"
}
},
"repair": {
"tags": "corrigir,restaurar,correção,recuperar",
@@ -1106,10 +1196,10 @@
"credit": "Usa WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "marcação,conteúdo-web,transformação,converter",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Converter"
},
"getPdfInfo": {
"tags": "informação,dados,estatísticas,estatísticas",
@@ -1174,7 +1264,22 @@
"showLayers": "Mostrar Camadas (duplo clique para repor todas as camadas para o estado predefinido)",
"colourPicker": "Seletor de Cor",
"findCurrentOutlineItem": "Encontrar item atual do esquema",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Avançado"
},
"wordsToRedact": {
"add": "Adicionar"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Páginas",
"placeholder": "(ex. 1,2,8 ou 4,7,12-16 ou 2n-1)"
},
"export": "Exportar"
}
},
"tableExtraxt": {
"tags": "CSV,Extração de Tabela,extrair,converter"
@@ -1386,7 +1491,10 @@
"4": "Modo automático - Ajusta automaticamente a qualidade para obter o PDF com o tamanho exato",
"5": "Tamanho esperado do PDF (ex. 25MB, 10.8MB, 25KB)"
},
"submit": "Comprimir"
"submit": "Comprimir",
"method": {
"filesize": "Tamanho do Ficheiro"
}
},
"decrypt": {
"passwordPrompt": "Este ficheiro está protegido por palavra-passe. Por favor introduza a palavra-passe:",
@@ -1557,5 +1665,82 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Transferir",
"convert": {
"title": "Converter",
"settings": "Definições",
"color": "Cor",
"greyscale": "Escala de Cinza",
"fillPage": "Preencher Página",
"pdfaDigitalSignatureWarning": "O PDF contém uma assinatura digital. Esta será removida no próximo passo.",
"grayscale": "Escala de Cinza"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Selecionar Tudo",
"deselectAll": "Desselecionar Tudo"
},
"quickAccess": {
"sign": "Assinar"
},
"fileUpload": {
"loading": "A carregar...",
"or": "ou"
},
"fileManager": {
"fileName": "Nome",
"fileVersion": "Versão",
"selectAll": "Selecionar Tudo",
"deselectAll": "Desselecionar Tudo",
"deleteSelected": "Eliminar Selecionadas",
"download": "Transferir",
"delete": "Eliminar"
},
"sanitize": {
"submit": "Sanitizar PDF",
"steps": {
"settings": "Definições"
}
},
"changePermissions": {
"title": "Alterar Permissões",
"submit": "Alterar Permissões",
"permissions": {
"preventAssembly": {
"label": "Impedir montagem do documento"
},
"preventExtractContent": {
"label": "Impedir extração de conteúdo"
},
"preventExtractForAccessibility": {
"label": "Impedir extração para acessibilidade"
},
"preventFillInForm": {
"label": "Impedir preenchimento de formulário"
},
"preventModify": {
"label": "Impedir modificação"
},
"preventModifyAnnotations": {
"label": "Impedir modificação de anotação"
},
"preventPrinting": {
"label": "Impedir impressão"
},
"preventPrintingFaithful": {
"label": "Impedir impressão em diferentes formatos"
}
},
"tooltip": {
"header": {
"title": "Alterar Permissões"
}
}
}
}
}
+190 -21
View File
@@ -137,7 +137,7 @@
"scanHelp": "Ajutor pentru Scanarea Dosarului",
"deletePrompt": "Sigur doriți să ștergeți pipeline-ul",
"tags": "automatizează,secvență,scriptare,procesare-lot",
"title": "Pipeline"
"title": "Pipeline (Avansat)"
},
"pipelineOptions": {
"header": "Configurare Pipeline",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "Convertește"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrage Pagini"
},
"removePages": {
"title": "Elimină",
"desc": "Șterge paginile nedorite din documentul PDF."
},
"removeImagePdf": {
"title": "Elimină imagine",
"desc": "Elimină imaginea din PDF pentru a reduce dimensiunea fișierului"
},
"autoSizeSplitPDF": {
"title": "Despărțire Automată după Dimensiune/Număr",
"desc": "Împarte un singur PDF în mai multe documente bazat pe dimensiune, număr de pagini sau număr de documente"
},
"adjust-contrast": {
"title": "Ajustează Culorile/Contrastul",
"desc": "Ajustează Contrastul, Saturația și Luminozitatea unui PDF"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "Schimbă Permisiunile"
}
},
"viewPdf": {
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Desparte",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Șterge",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sortează după nume",
"sortByDate": "Sortează după dată",
"removeCertSign": "Elimină semnătura digitală în fișierul unificat?",
"submit": "Unire"
"submit": "Unire",
"sortBy": {
"filename": "Nume Fișier"
}
},
"split": {
"tags": "Operații pagină,divizare,Pagină Multiplă,tăiere,server side",
@@ -634,7 +669,20 @@
"8": "Documentul #6: Pagina 10"
},
"splitPages": "Introduceți paginile pe care să le împărțiți:",
"submit": "Împarte"
"submit": "Împarte",
"steps": {
"settings": "Setări"
},
"methods": {
"bySize": {
"name": "Dimensiune Fișier"
}
},
"value": {
"fileSize": {
"label": "Dimensiune Fișier"
}
}
},
"rotate": {
"tags": "server side",
@@ -712,6 +760,9 @@
"type": {
"1": "Text",
"2": "Imagine"
},
"settings": {
"fontSize": "Dimensiune Font"
}
},
"permissions": {
@@ -734,7 +785,9 @@
"submit": "Schimbă"
},
"removePages": {
"tags": "Elimină pagini,șterge pagini"
"tags": "Elimină pagini,șterge pagini",
"title": "Elimină",
"submit": "Elimină"
},
"addPassword": {
"tags": "securizează,securitate",
@@ -758,7 +811,12 @@
"15": "Restricționează ce se poate face cu documentul odată ce este deschis (Nu este suportat de toate programele de citire)",
"16": "Restricționează deschiderea documentului în sine"
},
"submit": "Criptează"
"submit": "Criptează",
"tooltip": {
"permissions": {
"title": "Schimbă Permisiunile"
}
}
},
"removePassword": {
"tags": "securizează,Decriptează,securitate,elimină parola,șterge parola",
@@ -768,7 +826,12 @@
"1": "Selectează PDF-ul pentru decodificare",
"2": "Parolă"
},
"submit": "Elimină"
"submit": "Elimină",
"desc": "Elimină protecția cu parolă din documentul PDF.",
"password": {
"stepTitle": "Elimină Parola",
"label": "Parola Curentă"
}
},
"compressPdfs": {
"tags": "comprimă,mic,minuscul"
@@ -829,7 +892,25 @@
},
"help": "Citiți documentația pentru a afla cum să utilizați acest serviciu pentru alte limbi și/sau în afara mediului Docker",
"credit": "Acest serviciu utilizează qpdf și Tesseract pentru OCR.",
"submit": "Procesează PDF-ul cu OCR"
"submit": "Procesează PDF-ul cu OCR",
"desc": "Curăță scanările și detectează textul din imaginile dintr-un PDF și îl adaugă ca text.",
"settings": {
"title": "Setări",
"ocrMode": {
"label": "Mod OCR"
},
"languages": {
"label": "Limbi"
}
},
"tooltip": {
"mode": {
"title": "Mod OCR"
},
"languages": {
"title": "Limbi"
}
}
},
"extractImages": {
"tags": "poză,fotografie,salvează,arhivă,zip,captură,extrage",
@@ -924,7 +1005,7 @@
"sharedSigs": "Shared Signatures",
"noSavedSigs": "No saved signatures found",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Șterge",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1019,13 @@
"title": "Nivelare",
"header": "Nivelează documente PDF",
"flattenOnlyForms": "Nivelează doar formularele",
"submit": "Nivelează"
"submit": "Nivelează",
"steps": {
"settings": "Setări"
},
"options": {
"flattenOnlyForms": "Nivelează doar formularele"
}
},
"repair": {
"tags": "repară,restaurează,corectare,recuperează",
@@ -1106,10 +1193,10 @@
"credit": "Folosește WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,conținut-web,transformare,convertește",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Convertește"
},
"getPdfInfo": {
"tags": "informații,date,statistici,statistici",
@@ -1148,7 +1235,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1261,20 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Avansat"
},
"wordsToRedact": {
"add": "Adaugă"
}
},
"manual": {
"pageRedactionNumbers": {
"placeholder": "(ex. 1,2,8 sau 4,7,12-16 sau 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,Extragere Tabel,extrage,convertește"
@@ -1261,8 +1361,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Motivul",
"location": "Locația",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1381,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Versiune",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1486,10 @@
"4": "Mod automat - ajustează automat calitatea pentru a aduce PDF-ul la dimensiunea exactă",
"5": "Dimensiunea PDF așteptată (de ex. 25MB, 10.8MB, 25KB)"
},
"submit": "Comprimare"
"submit": "Comprimare",
"method": {
"filesize": "Dimensiune Fișier"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1501,7 +1604,7 @@
"3": "Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.",
"4": "Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs."
},
"submit": "Split PDF"
"submit": "Împarte PDF"
},
"fileChooser": {
"click": "Click",
@@ -1557,5 +1660,71 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Descarcă",
"convert": {
"title": "Convertește",
"settings": "Setări",
"color": "Culoare",
"greyscale": "Scală de gri",
"fillPage": "Umple Pagina",
"pdfaDigitalSignatureWarning": "PDF-ul conține o semnătură digitală. Aceasta va fi eliminată în pasul următor.",
"grayscale": "Scală de gri"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Semnează"
},
"fileManager": {
"fileName": "Numele",
"fileVersion": "Versiune",
"download": "Descarcă",
"delete": "Șterge"
},
"sanitize": {
"submit": "Igienizează PDF",
"steps": {
"settings": "Setări"
}
},
"changePermissions": {
"title": "Schimbă Permisiunile",
"submit": "Schimbă Permisiunile",
"permissions": {
"preventAssembly": {
"label": "Previne asamblarea documentului"
},
"preventExtractContent": {
"label": "Previne extragerea conținutului"
},
"preventExtractForAccessibility": {
"label": "Previne extragerea pentru accesibilitate"
},
"preventFillInForm": {
"label": "Previne completarea formularului"
},
"preventModify": {
"label": "Previne modificarea"
},
"preventModifyAnnotations": {
"label": "Previne modificarea adnotărilor"
},
"preventPrinting": {
"label": "Previne tipărirea"
},
"preventPrintingFaithful": {
"label": "Previne tipărirea în formate diferite"
}
},
"tooltip": {
"header": {
"title": "Schimbă Permisiunile"
}
}
}
}
}
+264 -73
View File
@@ -25,9 +25,9 @@
"multiPdfDropPrompt": "Выберите (или перетащите) все необходимые PDF-файлы",
"imgPrompt": "Выберите изображение(я)",
"genericSubmit": "Отправить",
"uploadLimit": "Maximum file size:",
"uploadLimitExceededSingular": "is too large. Maximum allowed size is",
"uploadLimitExceededPlural": "are too large. Maximum allowed size is",
"uploadLimit": "Максимальный размер файла:",
"uploadLimitExceededSingular": "слишком большой. Максимально допустимый размер",
"uploadLimitExceededPlural": "слишком большой. Максимально допустимый размер",
"processTimeWarning": "Внимание: Данный процесс может занять до минуты в зависимости от размера файла",
"pageOrderPrompt": "Пользовательский порядок страниц (Введите список номеров страниц через запятую или функции типа 2n+1):",
"pageSelectionPrompt": "Выбор страниц (Введите список номеров страниц через запятую 1,5,6 или функции типа 2n+1):",
@@ -118,14 +118,14 @@
"addToDoc": "Добавить в документ",
"reset": "Сбросить",
"apply": "Применить",
"noFileSelected": "No file selected. Please upload one.",
"noFileSelected": "Файл не выбран. Пожалуйста, загрузите его.",
"legal": {
"privacy": "Политика конфиденциальности",
"terms": "Условия использования",
"accessibility": "Доступность",
"cookie": "Политика использования файлов cookie",
"impressum": "Выходные данные",
"showCookieBanner": "Cookie Preferences"
"showCookieBanner": "Настройки файлов cookie"
},
"pipeline": {
"header": "Меню конвейера (Beta)",
@@ -265,7 +265,7 @@
"disabledUsers": "Отключенные пользователи:",
"totalUsers": "Всего пользователей:",
"lastRequest": "Последний запрос",
"usage": "View Usage"
"usage": "Просмотр использования"
},
"endpointStatistics": {
"title": "Статистика конечных точек",
@@ -320,17 +320,17 @@
"desc": "Ваше локальное решение для всех потребностей, связанных с PDF.",
"searchBar": "Поиск функций...",
"viewPdf": {
"title": "View/Edit PDF",
"title": "Смотреть/Редактировать PDF",
"desc": "Просмотр, аннотирование, добавление текста или изображений"
},
"setFavorites": "Set Favourites",
"hideFavorites": "Hide Favourites",
"showFavorites": "Show Favourites",
"legacyHomepage": "Old homepage",
"newHomePage": "Try our new homepage!",
"alphabetical": "Alphabetical",
"globalPopularity": "Global Popularity",
"sortBy": "Sort by:",
"setFavorites": "Добавить в избранное",
"hideFavorites": "Скрыть из избранного",
"showFavorites": "Показать избранное",
"legacyHomepage": "Старый вид главной страницы",
"newHomePage": "Попробуйте нашу новую главную страницу!",
"alphabetical": "Алфавиту",
"globalPopularity": "Популярности",
"sortBy": "Сортировать по:",
"multiTool": {
"title": "Мультиинструмент PDF",
"desc": "Объединение, поворот, переупорядочивание и удаление страниц"
@@ -388,8 +388,8 @@
"desc": "Сжимайте PDF-файлы для уменьшения их размера."
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "Разблокировать PDF-формы",
"desc": "Удалите свойство 'только для чтения' для полей формы в PDF-документа."
},
"changeMetadata": {
"title": "Изменить метаданные",
@@ -578,11 +578,48 @@
"replace-color": {
"title": "Замена и инверсия цвета",
"desc": "Заменяет цвет текста и фона в PDF и инвертирует все цвета PDF для уменьшения размера файла"
},
"convert": {
"title": "Преобразовать"
},
"attachments": {
"title": "Добавлять вложения",
"desc": "Добавление или удаление встроенных файлов (вложений) в PDF-файл или из него"
},
"editTableOfContents": {
"title": "Редактир оглавления",
"desc": "Добавление или редактирование закладок и оглавления в PDF-документах"
},
"extractPages": {
"title": "Извлечь страницы"
},
"removePages": {
"title": "Удалить",
"desc": "Удалите ненужные страницы из вашего PDF-документа."
},
"removeImagePdf": {
"title": "Удалить изображение",
"desc": "Удаляет изображения из PDF для уменьшения размера файла"
},
"autoSizeSplitPDF": {
"title": "Авторазделение по размеру/количеству",
"desc": "Разделяет один PDF на несколько документов на основе размера, количества страниц или количества документов"
},
"adjust-contrast": {
"title": "Настройка цветов/контраста",
"desc": "Настройка контраста, насыщенности и яркости PDF"
},
"replaceColorPdf": {
"title": "Замена и инверсия цвета",
"desc": "Заменяет цвет текста и фона в PDF и инвертирует все цвета PDF для уменьшения размера файла"
},
"changePermissions": {
"title": "Изменить разрешения"
}
},
"viewPdf": {
"tags": "просмотр,чтение,аннотации,текст,изображение",
"title": "View/Edit PDF",
"title": "Смотреть/Редактировать PDF",
"header": "Просмотр PDF"
},
"multiTool": {
@@ -617,7 +654,10 @@
"sortByName": "Сортировать по имени",
"sortByDate": "Сортировать по дате",
"removeCertSign": "Удалить цифровую подпись в объединенном файле?",
"submit": "Объединить"
"submit": "Объединить",
"sortBy": {
"filename": "Имя файла"
}
},
"split": {
"tags": "операции со страницами,разделение,многостраничный,вырезание,серверная часть",
@@ -634,7 +674,20 @@
"8": "Документ #6: Страница 10"
},
"splitPages": "Введите страницы для разделения:",
"submit": "Разделить"
"submit": "Разделить",
"steps": {
"settings": "Настройки"
},
"methods": {
"bySize": {
"name": "Размер файла"
}
},
"value": {
"fileSize": {
"label": "Размер файла"
}
}
},
"rotate": {
"tags": "серверная часть",
@@ -679,7 +732,7 @@
"8": "Удалить последнюю",
"9": "Удалить первую и последнюю",
"10": "Объединение четных-нечетных",
"11": "Duplicate all pages"
"11": "Дублировать все страницы"
},
"placeholder": "(например, 1,3,2 или 4-8,2,10-12 или 2n-1)"
},
@@ -712,6 +765,12 @@
"type": {
"1": "Текст",
"2": "Изображение"
},
"watermarkType": {
"text": "Текст"
},
"settings": {
"fontSize": "Размер шрифта"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "Изменить"
},
"removePages": {
"tags": "Удалить страницы,удаление страниц"
"tags": "Удалить страницы,удаление страниц",
"title": "Удалить",
"submit": "Удалить"
},
"addPassword": {
"tags": "безопасность,защита",
@@ -758,7 +819,12 @@
"15": "Ограничивает действия с документом после его открытия (Поддерживается не всеми программами чтения)",
"16": "Ограничивает само открытие документа"
},
"submit": "Зашифровать"
"submit": "Зашифровать",
"tooltip": {
"permissions": {
"title": "Изменить разрешения"
}
}
},
"removePassword": {
"tags": "безопасность,расшифровка,защита,удаление пароля",
@@ -768,15 +834,20 @@
"1": "Выберите PDF для расшифровки",
"2": "Пароль"
},
"submit": "Удалить"
"submit": "Удалить",
"desc": "Удалите защиту паролем из вашего PDF-документа.",
"password": {
"stepTitle": "Удалить пароль",
"label": "Текущий пароль"
}
},
"compressPdfs": {
"tags": "сжатие,маленький,крошечный"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"tags": "удалить,форма,поле,только для чтения",
"title": "Удалить поля формы, доступные только для чтения",
"header": "Разблокировать PDF-формы",
"submit": "Remove"
},
"changeMetadata": {
@@ -797,7 +868,7 @@
"modDate": "Дата изменения (yyyy/MM/dd HH:mm:ss):",
"producer": "Производитель:",
"subject": "Тема:",
"trapped": "Trapped:",
"trapped": "Захвачен:",
"submit": "Изменить"
},
"fileToPDF": {
@@ -829,7 +900,25 @@
},
"help": "Пожалуйста, прочтите эту документацию о том, как использовать это для других языков и/или использовать не в Docker",
"credit": "Этот сервис использует qpdf и Tesseract для OCR.",
"submit": "Обработать PDF с OCR"
"submit": "Обработать PDF с OCR",
"desc": "Очистка сканов и распознавание текста с изображений в PDF с последующим добавлением его как текст.",
"settings": {
"title": "Настройки",
"ocrMode": {
"label": "Режим OCR"
},
"languages": {
"label": "Языки"
}
},
"tooltip": {
"mode": {
"title": "Режим OCR"
},
"languages": {
"title": "Языки"
}
}
},
"extractImages": {
"tags": "картинка,фото,сохранение,архив,zip,захват,извлечение",
@@ -870,7 +959,7 @@
"submit": "Преобразовать"
},
"PDFToText": {
"tags": "richformat,richtextformat,rich text format",
"tags": "rtf,richformat,richtextformat,rich text format",
"title": "PDF в RTF (текст)",
"header": "PDF в RTF (текст)",
"selectText": {
@@ -930,15 +1019,21 @@
"next": "Следующая страница",
"previous": "Предыдущая страница",
"maintainRatio": "Переключить сохранение пропорций",
"undo": "Undo",
"redo": "Redo"
"undo": "Отменить",
"redo": "Повторить"
},
"flatten": {
"tags": "статический,деактивация,неинтерактивный,упрощение",
"title": "Сведение",
"header": "Сведение PDF",
"flattenOnlyForms": "Свести только формы",
"submit": "Свести"
"submit": "Свести",
"steps": {
"settings": "Настройки"
},
"options": {
"flattenOnlyForms": "Свести только формы"
}
},
"repair": {
"tags": "исправление,восстановление,коррекция,возврат",
@@ -1106,10 +1201,10 @@
"credit": "Использует WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"tags": "разметка,веб-контент,преобразование,конвертацтя,md",
"title": "PDF в Markdown",
"header": "PDF в Markdown",
"submit": "Преобразовать"
},
"getPdfInfo": {
"tags": "информация,данные,статистика,параметры",
@@ -1125,7 +1220,7 @@
"tags": "одна страница"
},
"showJS": {
"tags": "JS",
"tags": "JS,Javascript",
"title": "Показать Javascript",
"header": "Показать Javascript",
"downloadJS": "Скачать Javascript",
@@ -1174,7 +1269,22 @@
"showLayers": "Показать слои (двойной щелчок для сброса всех слоев к состоянию по умолчанию)",
"colourPicker": "Выбор цвета",
"findCurrentOutlineItem": "Найти текущий элемент структуры",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Расширенные"
},
"wordsToRedact": {
"add": "Добавить"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Страницы",
"placeholder": "(например, 1,2,8 или 4,7,12-16 или 2n-1)"
},
"export": "Экспорт"
}
},
"tableExtraxt": {
"tags": "CSV,Извлечение таблиц,извлечение,конвертация"
@@ -1336,7 +1446,7 @@
"alreadyLoggedIn": "Вы уже вошли в",
"alreadyLoggedIn2": "устройств(а). Пожалуйста, выйдите из этих устройств и попробуйте снова.",
"toManySessions": "У вас слишком много активных сессий",
"logoutMessage": "You have been logged out."
"logoutMessage": "Вы вышли из системы."
},
"pdfToSinglePage": {
"title": "PDF в одну страницу",
@@ -1355,10 +1465,10 @@
"selectText": {
"1": "Удалить JavaScript-действия",
"2": "Удалить встроенные файлы",
"3": "Remove XMP metadata",
"3": "Удалить XMP метаданные",
"4": "Удалить ссылки",
"5": "Удалить шрифты",
"6": "Remove Document Info Metadata"
"6": "Удалить метаданные с информацией о документе"
},
"submit": "Очистить PDF"
},
@@ -1380,13 +1490,16 @@
"selectText": {
"1": {
"_value": "Compression Settings",
"1": "1-3 PDF compression,</br> 4-6 lite image compression,</br> 7-9 intense image compression Will dramatically reduce image quality"
"1": "1-3 сжатие PDF,</br> 4-6 лёгкое сжатие изображений,</br> 7-9 интенсивное сжатие изображений (значительно снижает качество изображений)"
},
"2": "Уровень оптимизации:",
"4": "Автоматический режим - автоматически настраивает качество для получения точного размера PDF",
"5": "Ожидаемый размер PDF (например, 25MB, 10.8MB, 25KB)"
},
"submit": "Сжать"
"submit": "Сжать",
"method": {
"filesize": "Размер файла"
}
},
"decrypt": {
"passwordPrompt": "Этот файл защищен паролем. Пожалуйста, введите пароль:",
@@ -1472,15 +1585,15 @@
"button": "Пройти опрос",
"dontShowAgain": "Больше не показывать",
"meeting": {
"1": "If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session.",
"2": "This is a chance to:",
"3": "Get help with deployment, integrations, or troubleshooting",
"4": "Provide direct feedback on performance, edge cases, and feature gaps",
"5": "Help us refine Stirling PDF for real-world enterprise use",
"6": "If you're interested, you can book time with our team directly. (English speaking only)",
"7": "Looking forward to digging into your use cases and making Stirling PDF even better!",
"notInterested": "Not a business and/or interested in a meeting?",
"button": "Book meeting"
"1": "Если вы используете Stirling PDF в своей работе, мы будем рады с вами пообщаться. Мы предлагаем сеансы технической поддержки в обмен на 15-минутный сеанс знакомства с пользователями.",
"2": "Это возможность:",
"3": "Получить помощь в развертывании, интеграции или устранении неполадок",
"4": "Предоставляйте прямые отзывы о производительности, передовых решениях и недостатках в функциях",
"5": "Помогите нам усовершенствовать Stirling PDF для использования в реальных корпоративных условиях",
"6": "Если вы заинтересованы, вы можете забронировать время у нашей команды напрямую. (Только для говорящих по-английски)",
"7": "С нетерпением ждем возможности изучить ваши варианты использования и сделать Stirling PDF еще лучше!",
"notInterested": "Не занимаетесь бизнесом и/или не заинтересованы во встрече?",
"button": "Забронировать встречу"
}
},
"removeImage": {
@@ -1523,39 +1636,117 @@
},
"cookieBanner": {
"popUp": {
"title": "How we use Cookies",
"title": "Как мы используем файлы cookie",
"description": {
"1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.",
"1": "Мы используем файлы cookie и другие технологии, чтобы улучшить работу Stirling PDF для вас, это помогает нам совершенствовать наши инструменты и создавать функции, которые вам понравятся.",
"2": "If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
},
"acceptAllBtn": "Okay",
"acceptNecessaryBtn": "No Thanks",
"showPreferencesBtn": "Manage preferences"
"acceptAllBtn": "Хорошо",
"acceptNecessaryBtn": "Нет, спасибо",
"showPreferencesBtn": "Управление предпочтениями"
},
"preferencesModal": {
"title": "Consent Preferences Center",
"acceptAllBtn": "Accept all",
"acceptNecessaryBtn": "Reject all",
"savePreferencesBtn": "Save preferences",
"closeIconLabel": "Close modal",
"serviceCounterLabel": "Service|Services",
"subtitle": "Cookie Usage",
"title": "Центр согласия предпочтений",
"acceptAllBtn": "Принять все",
"acceptNecessaryBtn": "Отклонить все",
"savePreferencesBtn": "Сохранить предпочтения",
"closeIconLabel": "Закрыть модальное окно",
"serviceCounterLabel": "Сервис|Услуги",
"subtitle": "Использование файлов cookie",
"description": {
"1": "Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.",
"2": "Stirling PDF cannot—and will never—track or access the content of the documents you use.",
"3": "Your privacy and trust are at the core of what we do."
"1": "Stirling PDF использует файлы cookie и аналогичные технологии, чтобы улучшить ваш опыт и понять, как используются наши инструменты. Это помогает нам повышать производительность, разрабатывать функции, которые вам интересны, и обеспечивать постоянную поддержку наших пользователей.",
"2": "Stirling PDF не может — и никогда не будет — отслеживать или получать доступ к содержимому используемых вами документов.",
"3": "Ваша конфиденциальность и доверие лежат в основе того, что мы делаем."
},
"necessary": {
"title": {
"1": "Strictly Necessary Cookies",
"2": "Always Enabled"
"1": "Строго необходимые файлы cookie",
"2": "Всегда включены"
},
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off."
},
"analytics": {
"title": "Analytics",
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
"title": "Аналитика",
"description": "Эти файлы cookie помогают нам понять, как используются наши инструменты, чтобы мы могли сосредоточиться на создании функций, которые больше всего ценятся нашим сообществом. Будьте уверены — Stirling PDF не может и никогда не будет отслеживать содержание документов, с которыми вы работаете."
}
}
},
"download": "Скачать",
"undo": "Отменить",
"convert": {
"title": "Преобразовать",
"settings": "Настройки",
"color": "Цвет",
"greyscale": "Оттенки серого",
"fillPage": "Заполнить страницу",
"pdfaDigitalSignatureWarning": "PDF содержит цифровую подпись. Она будет удалена на следующем шаге.",
"grayscale": "Оттенки серого"
},
"attachments": {
"tags": "вставлять,прикреплять,файл,вложение,вложения",
"title": "Добавлять вложения",
"header": "Добавлять вложения",
"submit": "Добавлять вложения"
},
"rightRail": {
"selectAll": "Выбрать все",
"deselectAll": "Отменить выбор всех"
},
"quickAccess": {
"sign": "Подпись"
},
"fileUpload": {
"loading": "Загрузка...",
"or": "или"
},
"fileManager": {
"fileName": "Имя",
"fileVersion": "Версия",
"selectAll": "Выбрать все",
"deselectAll": "Отменить выбор всех",
"deleteSelected": "Удалить выбранные",
"download": "Скачать",
"delete": "Удалить"
},
"sanitize": {
"submit": "Очистить PDF",
"steps": {
"settings": "Настройки"
}
},
"changePermissions": {
"title": "Изменить разрешения",
"submit": "Изменить разрешения",
"permissions": {
"preventAssembly": {
"label": "Запретить компоновку документа"
},
"preventExtractContent": {
"label": "Запретить извлечение содержимого"
},
"preventExtractForAccessibility": {
"label": "Запретить извлечение для специальных возможностей"
},
"preventFillInForm": {
"label": "Запретить заполнение форм"
},
"preventModify": {
"label": "Запретить модификацию"
},
"preventModifyAnnotations": {
"label": "Запретить модификацию аннотаций"
},
"preventPrinting": {
"label": "Запретить печать"
},
"preventPrintingFaithful": {
"label": "Запретить печать в высоком качестве"
}
},
"tooltip": {
"header": {
"title": "Изменить разрешения"
}
}
}
}
}
+160 -20
View File
@@ -175,7 +175,7 @@
"search": "Search",
"sections": {
"organize": "Organize",
"convertTo": "Convert to PDF",
"convertTo": "Konvertovať do PDF",
"convertFrom": "Convert from PDF",
"security": "Sign & Security",
"advance": "Advanced",
@@ -332,7 +332,7 @@
"globalPopularity": "Global Popularity",
"sortBy": "Sort by:",
"multiTool": {
"title": "PDF Multi Tool",
"title": "PDF Multi Nástroj",
"desc": "Zlúčiť, otočiť, preusporiadať a odstrániť stránky"
},
"merge": {
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "Konvertovať"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrahovať stránky"
},
"removePages": {
"title": "Odstrániť",
"desc": "Odstrániť nechcené stránky z vášho PDF dokumentu."
},
"removeImagePdf": {
"title": "Remove image",
"desc": "Remove image from PDF to reduce file size"
},
"autoSizeSplitPDF": {
"title": "Automatické rozdelenie podľa veľkosti/počtu",
"desc": "Rozdelí jeden PDF na viacero dokumentov na základe veľkosti, počtu stránok alebo počtu dokumentov"
},
"adjust-contrast": {
"title": "Upraviť farby/kontrast",
"desc": "Upravte kontrast, sýtosť a jas PDF"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "Zmeniť povolenia"
}
},
"viewPdf": {
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Rozdeliť",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Vymazať",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -634,7 +666,10 @@
"8": "Dokument č. 6: Strana 10"
},
"splitPages": "Zadajte stránky na rozdelenie:",
"submit": "Rozdeliť"
"submit": "Rozdeliť",
"steps": {
"settings": "Nastavenia"
}
},
"rotate": {
"tags": "beží na serveri",
@@ -734,7 +769,9 @@
"submit": "Zmeniť"
},
"removePages": {
"tags": "Odstrániť stránky,vymazať stránky"
"tags": "Odstrániť stránky,vymazať stránky",
"title": "Odstrániť",
"submit": "Odstrániť"
},
"addPassword": {
"tags": "zaistiť,bezpečnosť",
@@ -758,7 +795,12 @@
"15": "Obmedzuje, čo môže byť vykonané s dokumentom po jeho otvorení (Nepodporované všetkými čítačmi)",
"16": "Obmedzuje samotné otvorenie dokumentu"
},
"submit": "Zašifrovať"
"submit": "Zašifrovať",
"tooltip": {
"permissions": {
"title": "Zmeniť povolenia"
}
}
},
"removePassword": {
"tags": "zaistiť,Dešifrovať,bezpečnosť,odheslovať,vymazať heslo",
@@ -768,7 +810,12 @@
"1": "Vyberte PDF na dešifrovanie",
"2": "Heslo"
},
"submit": "Odstrániť"
"submit": "Odstrániť",
"desc": "Odstrániť ochranu heslom z vášho PDF dokumentu.",
"password": {
"stepTitle": "Odstrániť heslo",
"label": "Aktuálne heslo"
}
},
"compressPdfs": {
"tags": "stlačiť,malé,drobné"
@@ -829,7 +876,19 @@
},
"help": "Prosím, prečítajte si túto dokumentáciu o tom, ako používať OCR pre iné jazyky a/alebo použitie mimo docker",
"credit": "Táto služba používa qpdf a Tesseract pre OCR.",
"submit": "Spracovať PDF s OCR"
"submit": "Spracovať PDF s OCR",
"desc": "Čistenie skenov a rozpoznanie textu z obrázkov v PDF a opätovné pridanie ako text.",
"settings": {
"title": "Nastavenia",
"ocrMode": {
"label": "OCR režim"
}
},
"tooltip": {
"mode": {
"title": "OCR režim"
}
}
},
"extractImages": {
"tags": "obrázok,fotografia,uložiť,archív,zip,zachytiť,chytiť",
@@ -924,7 +983,7 @@
"sharedSigs": "Shared Signatures",
"noSavedSigs": "No saved signatures found",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Vymazať",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +997,13 @@
"title": "Zploštiť",
"header": "Zploštiť PDF",
"flattenOnlyForms": "Zploštiť iba formuláre",
"submit": "Zploštiť"
"submit": "Zploštiť",
"steps": {
"settings": "Nastavenia"
},
"options": {
"flattenOnlyForms": "Zploštiť iba formuláre"
}
},
"repair": {
"tags": "opraviť,obnoviť,oprava,obnovenie",
@@ -1106,10 +1171,10 @@
"credit": "Používa WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup, webový obsah, transformácia, konvertovať",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Konvertovať"
},
"getPdfInfo": {
"tags": "informácie, údaje, štatistiky",
@@ -1148,7 +1213,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1239,17 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"wordsToRedact": {
"add": "Pridať"
}
},
"manual": {
"pageRedactionNumbers": {
"placeholder": "(napr. 1,2,8 alebo 4,7,12-16 alebo 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV, extrakcia tabuliek, extrahovať, konvertovať"
@@ -1261,8 +1336,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Dôvod",
"location": "Miesto",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1356,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Verzia",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1501,7 +1576,7 @@
"3": "Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.",
"4": "Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs."
},
"submit": "Split PDF"
"submit": "Rozdeliť PDF"
},
"fileChooser": {
"click": "Click",
@@ -1557,5 +1632,70 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Stiahnuť",
"convert": {
"title": "Konvertovať",
"settings": "Nastavenia",
"color": "Farba",
"greyscale": "Odtiene šedej",
"fillPage": "Vyplniť stránku",
"grayscale": "Odtiene šedej"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Podpísať"
},
"fileManager": {
"fileName": "Meno",
"fileVersion": "Verzia",
"download": "Stiahnuť",
"delete": "Vymazať"
},
"sanitize": {
"submit": "Vyčistiť PDF",
"steps": {
"settings": "Nastavenia"
}
},
"changePermissions": {
"title": "Zmeniť povolenia",
"submit": "Zmeniť povolenia",
"permissions": {
"preventAssembly": {
"label": "Zakázať zostavovanie dokumentu"
},
"preventExtractContent": {
"label": "Zakázať extrakciu obsahu"
},
"preventExtractForAccessibility": {
"label": "Zakázať extrakciu pre prístupnosť"
},
"preventFillInForm": {
"label": "Zakázať vypĺňanie formulárov"
},
"preventModify": {
"label": "Zakázať úpravy"
},
"preventModifyAnnotations": {
"label": "Zakázať úpravu anotácií"
},
"preventPrinting": {
"label": "Zakázať tlač"
},
"preventPrintingFaithful": {
"label": "Zakázať tlač rôznych formátov"
}
},
"tooltip": {
"header": {
"title": "Zmeniť povolenia"
}
}
}
}
}
+198 -12
View File
@@ -332,7 +332,7 @@
"globalPopularity": "Globalna priljubljenost",
"sortBy": "Razvrsti po:",
"multiTool": {
"title": "PDF Multi Tool",
"title": "PDF večnamensko orodje",
"desc": "Spoji, zavrti, prerazporedi, razdeli in odstrani strani"
},
"merge": {
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Napredne barvne možnosti",
"desc": "Zamenjaj barvo besedila in ozadja v PDF-ju in obrni celotno barvo PDF-ja, da zmanjšaš velikost datoteke"
},
"convert": {
"title": "Pretvori"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Izvleči strani"
},
"removePages": {
"title": "Odstrani",
"desc": "Izbrišite neželene strani iz dokumenta PDF."
},
"removeImagePdf": {
"title": "Odstrani sliko",
"desc": "Odstranite sliko iz PDF-ja, da zmanjšate velikost datoteke"
},
"autoSizeSplitPDF": {
"title": "Samodejna razdelitev po velikosti/številu",
"desc": "Razdeli en PDF na več dokumentov glede na velikost, število strani ali število dokumentov"
},
"adjust-contrast": {
"title": "Prilagodi barve/kontrast",
"desc": "Prilagodi kontrast, nasičenost in svetlost PDF-ja"
},
"replaceColorPdf": {
"title": "Napredne barvne možnosti",
"desc": "Zamenjaj barvo besedila in ozadja v PDF-ju in obrni celotno barvo PDF-ja, da zmanjšaš velikost datoteke"
},
"changePermissions": {
"title": "Spremeni dovoljenja"
}
},
"viewPdf": {
@@ -588,7 +620,7 @@
"multiTool": {
"tags": "Več orodij, več operacij, uporabniški vmesnik, klik povleci, sprednji del, odjemalska stran, interaktivno, nepremagljivo, premakni, izbriši, preseli, razdeli",
"title": "PDF večnamensko orodje",
"header": "PDF Multi Tool",
"header": "PDF večnamensko orodje",
"uploadPrompts": "Ime datoteke",
"selectAll": "Izberi vse",
"deselectAll": "Prekliči izbor vseh",
@@ -617,7 +649,10 @@
"sortByName": "Razvrsti po imenu",
"sortByDate": "Razvrsti po datumu",
"removeCertSign": "Odstraniti digitalni podpis v združeni datoteki?",
"submit": "Združi"
"submit": "Združi",
"sortBy": {
"filename": "Ime datoteke"
}
},
"split": {
"tags": "Operacije strani,deli,Multi Page,cut,strežniška stran",
@@ -634,7 +669,20 @@
"8": "Dokument #6: stran 10"
},
"splitPages": "Vnesite strani za razdelitev:",
"submit": "Razdeli"
"submit": "Razdeli",
"steps": {
"settings": "Nastavitve"
},
"methods": {
"bySize": {
"name": "Velikost datoteke"
}
},
"value": {
"fileSize": {
"label": "Velikost datoteke"
}
}
},
"rotate": {
"tags": "strežniška stran",
@@ -712,6 +760,12 @@
"type": {
"1": "Besedilo",
"2": "Slika"
},
"watermarkType": {
"text": "Besedilo"
},
"settings": {
"fontSize": "Velikost pisave"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Spremeni"
},
"removePages": {
"tags": "Odstrani strani, izbriši strani"
"tags": "Odstrani strani, izbriši strani",
"title": "Odstrani",
"submit": "Odstrani"
},
"addPassword": {
"tags": "varno,varnost",
@@ -758,7 +814,12 @@
"15": "Omejuje, kaj je mogoče storiti z dokumentom, ko je odprt (ni podprt za vse bralnike)",
"16": "Omeji odpiranje samega dokumenta"
},
"submit": "Šifriraj"
"submit": "Šifriraj",
"tooltip": {
"permissions": {
"title": "Spremeni dovoljenja"
}
}
},
"removePassword": {
"tags": "varno,dešifriranje,varnost,odstranitev gesla,brisanje gesla",
@@ -768,7 +829,12 @@
"1": "Izberite PDF za dešifriranje",
"2": "Geslo"
},
"submit": "Odstrani"
"submit": "Odstrani",
"desc": "Odstranite zaščito z geslom iz vašega dokumenta PDF.",
"password": {
"stepTitle": "Odstrani geslo",
"label": "Trenutno geslo"
}
},
"compressPdfs": {
"tags": "squish,small,tiny"
@@ -829,7 +895,25 @@
},
"help": "Prosimo, preberite to dokumentacijo o uporabi tega za druge jezike in/ali uporabi ne v dockerju",
"credit": "Ta storitev uporablja qpdf in Tesseract za OCR.",
"submit": "Obdelaj PDF z OCR"
"submit": "Obdelaj PDF z OCR",
"desc": "Cleanup skenira in zazna besedilo iz slik znotraj PDF-ja in ga ponovno doda kot besedilo.",
"settings": {
"title": "Nastavitve",
"ocrMode": {
"label": "Način OCR"
},
"languages": {
"label": "Jeziki"
}
},
"tooltip": {
"mode": {
"title": "Način OCR"
},
"languages": {
"title": "Jeziki"
}
}
},
"extractImages": {
"tags": "slika,fotografija,shrani,arhiv,zip,zajemi,zgrabi",
@@ -938,7 +1022,13 @@
"title": "Zravnaj",
"header": "Zravnaj PDF",
"flattenOnlyForms": "Splošči samo obrazce",
"submit": "Zravnaj"
"submit": "Zravnaj",
"steps": {
"settings": "Nastavitve"
},
"options": {
"flattenOnlyForms": "Splošči samo obrazce"
}
},
"repair": {
"tags": "popravi,obnovi,popravi,obnovi",
@@ -1174,7 +1264,22 @@
"showLayers": "Prikaži plasti (dvokliknite za ponastavitev vseh plasti na privzeto stanje)",
"colourPicker": "Izbirnik barv",
"findCurrentOutlineItem": "Poišči trenutno postavko orisa",
"applyChanges": "Uporabi spremembe"
"applyChanges": "Uporabi spremembe",
"auto": {
"settings": {
"advancedTitle": "Napredno"
},
"wordsToRedact": {
"add": "Dodaj"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Strani",
"placeholder": "(npr. 1,2,8 ali 4,7,12-16 ali 2n-1)"
},
"export": "Izvozi"
}
},
"tableExtraxt": {
"tags": "CSV, ekstrakcija tabele, ekstrah, pretvorba"
@@ -1386,7 +1491,10 @@
"4": "Samodejni način - Samodejno prilagodi kakovost, da dobi PDF na natančno velikost",
"5": "Pričakovana velikost PDF (npr. 25 MB, 10,8 MB, 25 KB)"
},
"submit": "Stisnite"
"submit": "Stisnite",
"method": {
"filesize": "Velikost datoteke"
}
},
"decrypt": {
"passwordPrompt": "Ta datoteka je zaščitena z geslom. Prosim vnesite geslo:",
@@ -1557,5 +1665,83 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Prenos",
"undo": "Razveljavi",
"convert": {
"title": "Pretvori",
"settings": "Nastavitve",
"color": "Barva",
"greyscale": "Sivine",
"fillPage": "Izpolni stran",
"pdfaDigitalSignatureWarning": "PDF vsebuje digitalni podpis. To bo odstranjeno v naslednjem koraku.",
"grayscale": "Sivine"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Izberi vse",
"deselectAll": "Prekliči izbor vseh"
},
"quickAccess": {
"sign": "Podpiši"
},
"fileUpload": {
"loading": "Nalaganje...",
"or": "ali"
},
"fileManager": {
"fileName": "Ime",
"fileVersion": "Različica",
"selectAll": "Izberi vse",
"deselectAll": "Prekliči izbor vseh",
"deleteSelected": "Izbriši izbrano",
"download": "Prenos",
"delete": "Izbriši"
},
"sanitize": {
"submit": "Prečisti PDF",
"steps": {
"settings": "Nastavitve"
}
},
"changePermissions": {
"title": "Spremeni dovoljenja",
"submit": "Spremeni dovoljenja",
"permissions": {
"preventAssembly": {
"label": "Prepreči sestavljanje dokumenta"
},
"preventExtractContent": {
"label": "Prepreči ekstrakcijo vsebine"
},
"preventExtractForAccessibility": {
"label": "Prepreči ekstrakcijo za dostopnost"
},
"preventFillInForm": {
"label": "Prepreči izpolnjevanje obrazca"
},
"preventModify": {
"label": "Prepreči spreminjanje"
},
"preventModifyAnnotations": {
"label": "Prepreči spreminjanje pripisov"
},
"preventPrinting": {
"label": "Prepreči tiskanje"
},
"preventPrintingFaithful": {
"label": "Prepreči tiskanje različnih formatov"
}
},
"tooltip": {
"header": {
"title": "Spremeni dovoljenja"
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+193 -14
View File
@@ -137,7 +137,7 @@
"scanHelp": "Hjälp för mappskanning",
"deletePrompt": "Är du säker på att du vill ta bort pipeline",
"tags": "automatisera,sekvens,skriptad,batchprocess",
"title": "Pipeline"
"title": "Pipeline (Avancerat)"
},
"pipelineOptions": {
"header": "Pipeline-konfiguration",
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "Laddar...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Ersätt och Invertera färg",
"desc": "Ersätt färg för text och bakgrund i PDF och invertera hela färgen på PDF för att minska filstorlek"
},
"convert": {
"title": "Konvertera"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Extrahera sidor"
},
"removePages": {
"title": "Ta bort",
"desc": "Ta bort oönskade sidor från ditt PDF-dokument."
},
"removeImagePdf": {
"title": "Ta bort bild",
"desc": "Ta bort bild från PDF för att minska filstorlek"
},
"autoSizeSplitPDF": {
"title": "Auto-dela efter storlek/antal",
"desc": "Dela en enda PDF till flera dokument baserat på storlek, sidantal eller dokumentantal"
},
"adjust-contrast": {
"title": "Justera färger/kontrast",
"desc": "Justera kontrast, mättnad och ljusstyrka i en PDF"
},
"replaceColorPdf": {
"title": "Ersätt och Invertera färg",
"desc": "Ersätt färg för text och bakgrund i PDF och invertera hela färgen på PDF för att minska filstorlek"
},
"changePermissions": {
"title": "Ändra behörigheter"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Sortera efter namn",
"sortByDate": "Sortera efter datum",
"removeCertSign": "Ta bort digital signatur i den sammanslagna filen?",
"submit": "Slå samman"
"submit": "Slå samman",
"sortBy": {
"filename": "Filnamn"
}
},
"split": {
"tags": "Sidoperationer,dela,Multisida,klippa,serversida",
@@ -634,7 +669,20 @@
"8": "Dokument #6: Sida 10"
},
"splitPages": "Ange sidor att dela på:",
"submit": "Dela"
"submit": "Dela",
"steps": {
"settings": "Inställningar"
},
"methods": {
"bySize": {
"name": "Filstorlek"
}
},
"value": {
"fileSize": {
"label": "Filstorlek"
}
}
},
"rotate": {
"tags": "serversida",
@@ -712,6 +760,9 @@
"type": {
"1": "Text",
"2": "Bild"
},
"settings": {
"fontSize": "Teckenstorlek"
}
},
"permissions": {
@@ -734,7 +785,9 @@
"submit": "Ändra"
},
"removePages": {
"tags": "Ta bort sidor,radera sidor"
"tags": "Ta bort sidor,radera sidor",
"title": "Ta bort",
"submit": "Ta bort"
},
"addPassword": {
"tags": "säkra,säkerhet",
@@ -758,7 +811,12 @@
"15": "Begränsar vad som kan göras med dokumentet när det väl är öppnat (Stöds inte av alla läsare)",
"16": "Begränsar öppnandet av själva dokumentet"
},
"submit": "Kryptera"
"submit": "Kryptera",
"tooltip": {
"permissions": {
"title": "Ändra behörigheter"
}
}
},
"removePassword": {
"tags": "säkra,Dekryptera,säkerhet,ta bort lösenord,radera lösenord",
@@ -768,7 +826,12 @@
"1": "Välj PDF att dekryptera",
"2": "Lösenord"
},
"submit": "Ta bort"
"submit": "Ta bort",
"desc": "Ta bort lösenordsskydd från ditt PDF-dokument.",
"password": {
"stepTitle": "Ta bort lösenord",
"label": "Nuvarande lösenord"
}
},
"compressPdfs": {
"tags": "pressa ihop,liten,minimal"
@@ -829,7 +892,25 @@
},
"help": "Vänligen läs denna dokumentation om hur du använder detta för andra språk och/eller använder inte i docker",
"credit": "Denna tjänst använder qpdf och Tesseract för OCR.",
"submit": "Bearbeta PDF med OCR"
"submit": "Bearbeta PDF med OCR",
"desc": "Rengör skanningar och upptäcker text från bilder i en PDF och lägger till den igen som text.",
"settings": {
"title": "Inställningar",
"ocrMode": {
"label": "OCR-läge"
},
"languages": {
"label": "Språk"
}
},
"tooltip": {
"mode": {
"title": "OCR-läge"
},
"languages": {
"title": "Språk"
}
}
},
"extractImages": {
"tags": "bild,foto,spara,arkiv,zip,fånga,ta",
@@ -938,7 +1019,13 @@
"title": "Platta till",
"header": "Platta till PDF-filer",
"flattenOnlyForms": "Platta till endast formulär",
"submit": "Platta till"
"submit": "Platta till",
"steps": {
"settings": "Inställningar"
},
"options": {
"flattenOnlyForms": "Platta till endast formulär"
}
},
"repair": {
"tags": "fixa,återställa,korrigering,återhämta",
@@ -1106,7 +1193,7 @@
"credit": "Använder WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "markup,webbinnehåll,transformation,konvertera",
"title": "PDF Till Markdown",
"header": "PDF Till Markdown",
"submit": "Konvertera"
@@ -1148,7 +1235,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1261,22 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Avancerat"
},
"wordsToRedact": {
"add": "Lägg till"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Sidor",
"placeholder": "(t.ex. 1,2,8 eller 4,7,12-16 eller 2n-1)"
},
"export": "Exportera"
}
},
"tableExtraxt": {
"tags": "CSV,Tabellextraktion,extrahera,konvertera"
@@ -1386,7 +1488,10 @@
"4": "Autoläge - Autojusterar kvaliteten för att få PDF till exakt storlek",
"5": "Förväntad PDF-storlek (t.ex. 25MB, 10,8MB, 25KB)"
},
"submit": "Komprimera"
"submit": "Komprimera",
"method": {
"filesize": "Filstorlek"
}
},
"decrypt": {
"passwordPrompt": "Denna fil är lösenordsskyddad. Fyll i lösenord:",
@@ -1557,5 +1662,79 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Ladda ner",
"convert": {
"title": "Konvertera",
"settings": "Inställningar",
"color": "Färg",
"greyscale": "Gråskala",
"fillPage": "Fyll sida",
"pdfaDigitalSignatureWarning": "PDF:en innehåller en digital signatur. Denna kommer att tas bort i nästa steg.",
"grayscale": "Gråskala"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Välj allt"
},
"quickAccess": {
"sign": "Signera"
},
"fileUpload": {
"loading": "Laddar...",
"or": "eller"
},
"fileManager": {
"fileName": "Namn",
"selectAll": "Välj allt",
"deleteSelected": "Ta bort valda",
"download": "Ladda ner",
"delete": "Radera"
},
"sanitize": {
"submit": "Sanera PDF",
"steps": {
"settings": "Inställningar"
}
},
"changePermissions": {
"title": "Ändra behörigheter",
"submit": "Ändra behörigheter",
"permissions": {
"preventAssembly": {
"label": "Förhindra sammansättning av dokument"
},
"preventExtractContent": {
"label": "Förhindra innehållsextraktion"
},
"preventExtractForAccessibility": {
"label": "Förhindra extraktion för tillgänglighet"
},
"preventFillInForm": {
"label": "Förhindra att fylla i formulär"
},
"preventModify": {
"label": "Förhindra ändring"
},
"preventModifyAnnotations": {
"label": "Förhindra anteckningsändring"
},
"preventPrinting": {
"label": "Förhindra utskrift"
},
"preventPrintingFaithful": {
"label": "Förhindra utskrift av olika format"
}
},
"tooltip": {
"header": {
"title": "Ändra behörigheter"
}
}
}
}
}
+198 -22
View File
@@ -283,7 +283,7 @@
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"loading": "กำลังโหลด...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
@@ -568,7 +568,7 @@
"desc": "ลบภาพออกจาก PDF เพื่อลดขนาดไฟล์"
},
"splitByChapters": {
"title": "Split PDF by Chapters",
"title": "แบ่งไฟล์ PDF ตามหมวดหมู่",
"desc": "Split a PDF into multiple files based on its chapter structure."
},
"validateSignature": {
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "แปลง"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "แยกหน้า"
},
"removePages": {
"title": "ลบ",
"desc": "ลบหน้าที่ไม่ต้องการจากเอกสาร PDF ของคุณ"
},
"removeImagePdf": {
"title": "ลบภาพออกจาก PDF",
"desc": "ลบภาพออกจาก PDF เพื่อลดขนาดไฟล์"
},
"autoSizeSplitPDF": {
"title": "แยกตามขนาด/จำนวน",
"desc": "แยก PDF เป็นเอกสารหลายฉบับตามขนาด จำนวนหน้า หรือจำนวนเอกสาร"
},
"adjust-contrast": {
"title": "ปรับสี/คอนทราสต์",
"desc": "ปรับคอนทราสต์ ความอิ่มตัว และความสว่างของ PDF"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "เปลี่ยนสิทธิ์"
}
},
"viewPdf": {
@@ -594,7 +626,7 @@
"deselectAll": "Deselect All",
"selectPages": "Page Select",
"selectedPages": "Selected Pages",
"page": "Page",
"page": "หน้า",
"deleteSelected": "Delete Selected",
"downloadAll": "Export",
"downloadSelected": "Export Selected",
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "แยกไฟล์ PDF",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "ลบ",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "จัดเรียงตามชื่อ",
"sortByDate": "จัดเรียงตามวันที่",
"removeCertSign": "ลบลายเซ็นดิจิทัลในไฟล์ที่รวม?",
"submit": "รวม"
"submit": "รวม",
"sortBy": {
"filename": "ชื่อไฟล์"
}
},
"split": {
"tags": "การดำเนินการหน้า, แบ่ง, หลายหน้า, ตัด, ฝั่งเซิร์ฟเวอร์",
@@ -634,7 +669,20 @@
"8": "เอกสาร #6: หน้า 10"
},
"splitPages": "ป้อนหน้าที่ต้องการแยก:",
"submit": "แยก"
"submit": "แยก",
"steps": {
"settings": "การตั้งค่า"
},
"methods": {
"bySize": {
"name": "ขนาดไฟล์"
}
},
"value": {
"fileSize": {
"label": "ขนาดไฟล์"
}
}
},
"rotate": {
"tags": "ฝั่งเซิร์ฟเวอร์",
@@ -712,6 +760,12 @@
"type": {
"1": "ข้อความ",
"2": "ภาพ"
},
"watermarkType": {
"text": "ข้อความ"
},
"settings": {
"fontSize": "ขนาดตัวอักษร"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "เปลี่ยน"
},
"removePages": {
"tags": "ลบหน้า, ลบหน้า"
"tags": "ลบหน้า, ลบหน้า",
"title": "ลบ",
"submit": "ลบ"
},
"addPassword": {
"tags": "ปลอดภัย, ความปลอดภัย",
@@ -758,7 +814,12 @@
"15": "จำกัดสิ่งที่สามารถทำได้กับเอกสารเมื่อเปิด (ไม่รองรับโดยผู้อ่านทั้งหมด)",
"16": "จำกัดการเปิดเอกสาร"
},
"submit": "เข้ารหัส"
"submit": "เข้ารหัส",
"tooltip": {
"permissions": {
"title": "เปลี่ยนสิทธิ์"
}
}
},
"removePassword": {
"tags": "ปลอดภัย, ถอดรหัส, ความปลอดภัย, ลบรหัสผ่าน",
@@ -768,7 +829,12 @@
"1": "เลือก PDF ที่ต้องการถอดรหัส",
"2": "รหัสผ่าน"
},
"submit": "ลบ"
"submit": "ลบ",
"desc": "ลบรหัสผ่านจากการป้องกันเอกสาร PDF ของคุณ",
"password": {
"stepTitle": "ลบรหัสผ่าน",
"label": "รหัสผ่านปัจจุบัน"
}
},
"compressPdfs": {
"tags": "ย่อ, เล็ก, จิ๋ว"
@@ -829,7 +895,25 @@
},
"help": "โปรดอ่านเอกสารนี้เพื่อใช้งานภาษาอื่นๆ และ/หรือใช้งานนอก docker",
"credit": "บริการนี้ใช้ qpdf และ Tesseract สำหรับ OCR",
"submit": "ประมวลผล PDF ด้วย OCR"
"submit": "ประมวลผล PDF ด้วย OCR",
"desc": "ทำความสะอาดการสแกนและตรวจจับข้อความจากภาพภายใน PDF และเพิ่มเป็นข้อความอีกครั้ง",
"settings": {
"title": "การตั้งค่า",
"ocrMode": {
"label": "โหมด OCR"
},
"languages": {
"label": "ภาษา"
}
},
"tooltip": {
"mode": {
"title": "โหมด OCR"
},
"languages": {
"title": "ภาษา"
}
}
},
"extractImages": {
"tags": "รูปภาพ, ภาพ, บันทึก, เก็บถาวร, zip, จับ, รับ",
@@ -924,7 +1008,7 @@
"sharedSigs": "ลายเซ็นร่วม",
"noSavedSigs": "ไม่พบลายเซ็นที่บันทึกไว้",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "ลบ",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1022,13 @@
"title": "แบน",
"header": "แบน PDF",
"flattenOnlyForms": "แบนเฉพาะฟอร์ม",
"submit": "แบน"
"submit": "แบน",
"steps": {
"settings": "การตั้งค่า"
},
"options": {
"flattenOnlyForms": "แบนเฉพาะฟอร์ม"
}
},
"repair": {
"tags": "ซ่อมแซม, กู้คืน, กู้",
@@ -1106,10 +1196,10 @@
"credit": "ใช้ WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "มาร์กอัป, เนื้อหาเว็บ, การแปลง, การแปลง",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "แปลง"
},
"getPdfInfo": {
"tags": "ข้อมูล, สถิติ, สถิติ",
@@ -1148,7 +1238,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1264,21 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "ขั้นสูง"
},
"wordsToRedact": {
"add": "เพิ่ม"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "หน้า",
"placeholder": "(เช่น 1,2,8 หรือ 4,7,12-16 หรือ 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV, การแยกตาราง, แยก, การแปลง"
@@ -1261,8 +1365,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "เหตุผล",
"location": "ตำแหน่ง",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1385,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "เวอร์ชัน",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1490,10 @@
"4": "โหมดอัตโนมัติ - ปรับคุณภาพอัตโนมัติเพื่อให้ PDF ตรงกับขนาดที่ต้องการ",
"5": "ขนาด PDF ที่คาดหวัง (เช่น 25MB, 10.8MB, 25KB)"
},
"submit": "บีบอัด"
"submit": "บีบอัด",
"method": {
"filesize": "ขนาดไฟล์"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1557,5 +1664,74 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "ดาวน์โหลด",
"convert": {
"title": "แปลง",
"settings": "การตั้งค่า",
"color": "สี",
"greyscale": "ระดับสีเทา",
"fillPage": "เติมหน้า",
"pdfaDigitalSignatureWarning": "PDF มีลายเซ็นดิจิทัล ซึ่งจะถูกลบในขั้นตอนถัดไป",
"grayscale": "ระดับสีเทา"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "เซ็นชื่อ"
},
"fileUpload": {
"loading": "กำลังโหลด..."
},
"fileManager": {
"fileName": "ชื่อ",
"fileVersion": "เวอร์ชัน",
"download": "ดาวน์โหลด",
"delete": "ลบ"
},
"sanitize": {
"submit": "ทำความสะอาด PDF",
"steps": {
"settings": "การตั้งค่า"
}
},
"changePermissions": {
"title": "เปลี่ยนสิทธิ์",
"submit": "เปลี่ยนสิทธิ์",
"permissions": {
"preventAssembly": {
"label": "ป้องกันการประกอบเอกสาร"
},
"preventExtractContent": {
"label": "ป้องกันการสกัดเนื้อหา"
},
"preventExtractForAccessibility": {
"label": "ป้องกันการสกัดเพื่อการเข้าถึง"
},
"preventFillInForm": {
"label": "ป้องกันการกรอกแบบฟอร์ม"
},
"preventModify": {
"label": "ป้องกันการแก้ไข"
},
"preventModifyAnnotations": {
"label": "ป้องกันการแก้ไขคำอธิบายประกอบ"
},
"preventPrinting": {
"label": "ป้องกันการพิมพ์"
},
"preventPrintingFaithful": {
"label": "ป้องกันการพิมพ์รูปแบบต่างๆ"
}
},
"tooltip": {
"header": {
"title": "เปลี่ยนสิทธิ์"
}
}
}
}
}
+208 -18
View File
@@ -172,7 +172,7 @@
"settings": "Ayarlar",
"allTools": "Araçlar",
"multiTool": "Çoklu Araçlar",
"search": "Search",
"search": "Ara",
"sections": {
"organize": "Düzenle",
"convertTo": "PDF'ye dönüştür",
@@ -320,7 +320,7 @@
"desc": "Yerel olarak barındırılan tüm PDF ihtiyaçlarınız için tek durak noktanız.",
"searchBar": "Özellikleri arayın...",
"viewPdf": {
"title": "View/Edit PDF",
"title": "PDF Görüntüle/Düzenle",
"desc": "Görüntüleyin, açıklama ekleyin, metin veya resim ekleyin"
},
"setFavorites": "Favorilere Ekle",
@@ -388,8 +388,8 @@
"desc": "PDF'lerin dosya boyutunu azaltmak için sıkıştırın."
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "PDF Formlarının Kilidini Kaldır",
"desc": "Form alanlarını düzenlenebilir hâle getir."
},
"changeMetadata": {
"title": "Metaveriyi Değiştir",
@@ -578,11 +578,47 @@
"replace-color": {
"title": "Renkleri Değiştir ve Tersine Çevir",
"desc": "PDF'deki metin ve arka plan renklerini değiştirin ve PDF'nin tüm renklerini tersine çevirerek dosya boyutunu azaltın"
},
"convert": {
"title": "Dönüştür"
},
"attachments": {
"title": "Add attachments"
},
"editTableOfContents": {
"title": "İçindekiler Tablosunu Düzenle",
"desc": "PDF belgelerinde yer işaretleri ve içindekiler tablosu ekleyin veya düzenleyin"
},
"extractPages": {
"title": "Sayfaları Çıkar"
},
"removePages": {
"title": "Kaldır",
"desc": "PDF belgenizden istenmeyen sayfaları silin."
},
"removeImagePdf": {
"title": "Resmi kaldır",
"desc": "Dosya boyutunu küçültmek için PDF'den resmi kaldırın"
},
"autoSizeSplitPDF": {
"title": "Boyut/Sayıya Göre Otomatik Bölme",
"desc": "Tek bir PDF'yi boyut, sayfa sayısı veya belge sayısına göre birden fazla belgeye bölün"
},
"adjust-contrast": {
"title": "Renkleri/Kontrastı Ayarla",
"desc": "Bir PDF'in Kontrastını, Doygunluğunu ve Parlaklığını ayarlar"
},
"replaceColorPdf": {
"title": "Renkleri Değiştir ve Tersine Çevir",
"desc": "PDF'deki metin ve arka plan renklerini değiştirin ve PDF'nin tüm renklerini tersine çevirerek dosya boyutunu azaltın"
},
"changePermissions": {
"title": "İzinleri Değiştir"
}
},
"viewPdf": {
"tags": "görüntüle,oku,açıklama ekle,metin,görüntü",
"title": "View/Edit PDF",
"title": "PDF Görüntüle/Düzenle",
"header": "PDF Görüntüle"
},
"multiTool": {
@@ -617,7 +653,10 @@
"sortByName": "İsme göre sırala",
"sortByDate": "Tarihe göre sırala",
"removeCertSign": "Birleştirilen dosyadaki dijital imza kaldırılsın mı?",
"submit": "Birleştir"
"submit": "Birleştir",
"sortBy": {
"filename": "Dosya Adı"
}
},
"split": {
"tags": "Sayfa işlemleri,böl,Çoklu Sayfa,kes,sunucu tarafı",
@@ -634,7 +673,20 @@
"8": "Belge #6: Sayfa 10"
},
"splitPages": "Ayrılacak sayfaları girin:",
"submit": "Ayır"
"submit": "Ayır",
"steps": {
"settings": "Ayarlar"
},
"methods": {
"bySize": {
"name": "Dosya Boyutu"
}
},
"value": {
"fileSize": {
"label": "Dosya Boyutu"
}
}
},
"rotate": {
"tags": "sunucu tarafı",
@@ -712,6 +764,12 @@
"type": {
"1": "Metin",
"2": "Resim"
},
"watermarkType": {
"text": "Metin"
},
"settings": {
"fontSize": "Font Büyüklüğü"
}
},
"permissions": {
@@ -734,7 +792,9 @@
"submit": "Değiştir"
},
"removePages": {
"tags": "Sayfaları kaldır,sayfaları sil"
"tags": "Sayfaları kaldır,sayfaları sil",
"title": "Kaldır",
"submit": "Kaldır"
},
"addPassword": {
"tags": "güvenli, güvenlik",
@@ -758,7 +818,12 @@
"15": "Açıldığında belgeyle ne yapılacağını kısıtlar (Tüm okuyucular tarafından desteklenmez)",
"16": "Belgenin kendisinin açılmasını kısıtlar"
},
"submit": "Şifrele"
"submit": "Şifrele",
"tooltip": {
"permissions": {
"title": "İzinleri Değiştir"
}
}
},
"removePassword": {
"tags": "güvenli,Şifreyi çöz,güvenlik,parolasız,parolayı sil",
@@ -768,15 +833,20 @@
"1": "Şifreyi Çözmek için PDF Seçin",
"2": "Parola"
},
"submit": "Kaldır"
"submit": "Kaldır",
"desc": "PDF belgenizden parola korumasını kaldırın.",
"password": {
"stepTitle": "Parolayı Kaldır",
"label": "Mevcut Şifre"
}
},
"compressPdfs": {
"tags": "sıkıştır,küçük,minik"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"title": "Form Alanlarının Salt Okunur Özelliğini Kaldır",
"header": "PDF Formlarının Kilidini Aç",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +899,25 @@
},
"help": "Lütfen bu belgede başka dillerde nasıl kullanılacağı ve/veya docker'da kullanılmaması hakkında bilgi edinin",
"credit": "Bu hizmet OCR için qpdf ve Tesseract'ı kullanır.",
"submit": "PDF'i OCR(Metin Tanıma) ile İşle"
"submit": "PDF'i OCR(Metin Tanıma) ile İşle",
"desc": "Taramaları temizler ve bir PDF içindeki resimlerden metni algılar ve tekrar metin olarak ekler.",
"settings": {
"title": "Ayarlar",
"ocrMode": {
"label": "OCR Modu"
},
"languages": {
"label": "Diller"
}
},
"tooltip": {
"mode": {
"title": "OCR Modu"
},
"languages": {
"title": "Diller"
}
}
},
"extractImages": {
"tags": "fotoğraf,resim,kaydet,arşiv,zip,yakala,al",
@@ -938,7 +1026,13 @@
"title": "Düzleştir",
"header": "PDF'leri Düzleştir",
"flattenOnlyForms": "Yalnızca formları düzleştir",
"submit": "Düzleştir"
"submit": "Düzleştir",
"steps": {
"settings": "Ayarlar"
},
"options": {
"flattenOnlyForms": "Yalnızca formları düzleştir"
}
},
"repair": {
"tags": "onar,geri yükle,düzelt,geri getir",
@@ -1109,7 +1203,7 @@
"tags": "biçimlendirme,web-içeriği,dönüşüm,dönüştür,md",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Dönüştür"
},
"getPdfInfo": {
"tags": "bilgi,veri,istatistikler,istatistik",
@@ -1174,7 +1268,22 @@
"showLayers": "Katmanları Göster (tüm katmanları varsayılana döndürmek için çift tıklayın)",
"colourPicker": "Renk Seçici",
"findCurrentOutlineItem": "Geçerli Anahat Öğesini Bul",
"applyChanges": "Değişiklikleri Uygula"
"applyChanges": "Değişiklikleri Uygula",
"auto": {
"settings": {
"advancedTitle": "Gelişmiş"
},
"wordsToRedact": {
"add": "Ekle"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Sayfalar",
"placeholder": "(örneğin 1,2,8 veya 4,7,12-16 ya da 2n-1)"
},
"export": "Dışa Aktar"
}
},
"tableExtraxt": {
"tags": "CSV, Tablo Çıkarma, ayıklama, dönüştürme"
@@ -1386,7 +1495,10 @@
"4": "Otomatik mod - PDF'in tam boyutuna ulaşmak için kaliteyi otomatik ayarlar",
"5": "Beklenen PDF Boyutu (örn. 25MB, 10.8MB, 25KB)"
},
"submit": "Sıkıştır"
"submit": "Sıkıştır",
"method": {
"filesize": "Dosya Boyutu"
}
},
"decrypt": {
"passwordPrompt": "Bu dosya parola korumalı. Lütfen parolayı girin:",
@@ -1557,5 +1669,83 @@
"description": "Bu çerezler, araçlarımızın nasıl kullanıldığını anlamamıza yardımcı olur, böylece topluluğumuzun en çok değer verdiği özellikleri geliştirmeye odaklanabiliriz. İçiniz rahat olsun — Stirling PDF, belgelerinizin içeriğini asla takip etmez ve etmeyecektir."
}
}
},
"download": "İndir",
"undo": "Geri Al",
"convert": {
"title": "Dönüştür",
"settings": "Ayarlar",
"color": "Renk",
"greyscale": "Gri tonlama",
"fillPage": "Sayfayı Doldur",
"pdfaDigitalSignatureWarning": "PDF dijital imza içeriyor. Bu bir sonraki adımda kaldırılacak.",
"grayscale": "Gri tonlama"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Tümünü Seç",
"deselectAll": "Seçimi Kaldır"
},
"quickAccess": {
"sign": "İmzala"
},
"fileUpload": {
"loading": "Yükleniyor...",
"or": "veya"
},
"fileManager": {
"fileName": "İsim",
"fileVersion": "Sürüm",
"selectAll": "Tümünü Seç",
"deselectAll": "Seçimi Kaldır",
"deleteSelected": "Seçilenleri Sil",
"download": "İndir",
"delete": "Sil"
},
"sanitize": {
"submit": "PDF'i Temizle",
"steps": {
"settings": "Ayarlar"
}
},
"changePermissions": {
"title": "İzinleri Değiştir",
"submit": "İzinleri Değiştir",
"permissions": {
"preventAssembly": {
"label": "Belgenin birleştirilmesini önle"
},
"preventExtractContent": {
"label": "İçeriğin çıkarılmasını önle"
},
"preventExtractForAccessibility": {
"label": "Erişilebilirlik için çıkarmanın önlenmesi"
},
"preventFillInForm": {
"label": "Formun doldurulmasını önle"
},
"preventModify": {
"label": "Değişikliği önle"
},
"preventModifyAnnotations": {
"label": "Açıklama değişikliğini önle"
},
"preventPrinting": {
"label": "Yazdırmayı önle"
},
"preventPrintingFaithful": {
"label": "Farklı formatlarda yazdırmayı önle"
}
},
"tooltip": {
"header": {
"title": "İzinleri Değiştir"
}
}
}
}
}
+196 -10
View File
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Заміна та інверсія кольору",
"desc": "Замінює колір тексту та фону у PDF та інвертує всі кольори PDF для зменшення розміру файлу"
},
"convert": {
"title": "Конвертувати"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Видобути сторінки"
},
"removePages": {
"title": "Видалення",
"desc": "Видаліть непотрібні сторінки з документа PDF."
},
"removeImagePdf": {
"title": "Видалити зображення",
"desc": "Видаляє зображення з PDF для зменшення розміру файлу"
},
"autoSizeSplitPDF": {
"title": "Автоматичне розділення за розміром/кількістю",
"desc": "Розділяє один PDF на кілька документів на основі розміру, кількості сторінок або кількості документів"
},
"adjust-contrast": {
"title": "Налаштування кольорів/контрастності",
"desc": "Налаштування контрастності, насиченості та яскравості файлу PDF"
},
"replaceColorPdf": {
"title": "Заміна та інверсія кольору",
"desc": "Замінює колір тексту та фону у PDF та інвертує всі кольори PDF для зменшення розміру файлу"
},
"changePermissions": {
"title": "Змінити дозволи"
}
},
"viewPdf": {
@@ -617,7 +649,10 @@
"sortByName": "Сортування за ім'ям",
"sortByDate": "Сортування за датою",
"removeCertSign": "Видалити цифровий підпис у об’єднаному файлі?",
"submit": "Об'єднати"
"submit": "Об'єднати",
"sortBy": {
"filename": "Ім'я файлу"
}
},
"split": {
"tags": "операції зі сторінками,розділення,багатосторінковий,вирізання,серверна частина",
@@ -634,7 +669,20 @@
"8": "Документ #6: Сторінки 9 і 10"
},
"splitPages": "Введіть сторінки для розділення:",
"submit": "Розділити"
"submit": "Розділити",
"steps": {
"settings": "Налаштування"
},
"methods": {
"bySize": {
"name": "Розмір файлу"
}
},
"value": {
"fileSize": {
"label": "Розмір файлу"
}
}
},
"rotate": {
"tags": "серверна частина",
@@ -712,6 +760,12 @@
"type": {
"1": "Текст",
"2": "Зображення"
},
"watermarkType": {
"text": "Текст"
},
"settings": {
"fontSize": "Розмір шрифту"
}
},
"permissions": {
@@ -734,7 +788,9 @@
"submit": "Змінити"
},
"removePages": {
"tags": "видалити сторінки,видалення сторінок"
"tags": "видалити сторінки,видалення сторінок",
"title": "Видалення",
"submit": "Видалення"
},
"addPassword": {
"tags": "безпека,захист",
@@ -758,7 +814,12 @@
"15": "Обмежує, що можна робити з документом після його відкриття (не підтримується всіма програмами читання)",
"16": "Обмежує відкриття самого документа"
},
"submit": "Шифрувати"
"submit": "Шифрувати",
"tooltip": {
"permissions": {
"title": "Змінити дозволи"
}
}
},
"removePassword": {
"tags": "безпека,розшифровка,захист,видалення пароля",
@@ -768,7 +829,12 @@
"1": "Виберіть PDF для розшифрування",
"2": "Пароль"
},
"submit": "Видалити"
"submit": "Видалити",
"desc": "Зніміть захист паролем з вашого документа PDF.",
"password": {
"stepTitle": "Видалити пароль",
"label": "Поточний пароль"
}
},
"compressPdfs": {
"tags": "стиск,маленький,крихітний"
@@ -829,7 +895,25 @@
},
"help": "Прочитайте цю документацію про те, як використовувати це для інших мов і/або використовувати не в докері.",
"credit": "Цей сервіс використовує qpdf та Tesseract для OCR.",
"submit": "Обробка PDF з OCR"
"submit": "Обробка PDF з OCR",
"desc": "Очищення сканування та виявлення тексту на зображеннях у файлі PDF та повторне додавання його як текст.",
"settings": {
"title": "Налаштування",
"ocrMode": {
"label": "Режим OCR"
},
"languages": {
"label": "Мови"
}
},
"tooltip": {
"mode": {
"title": "Режим OCR"
},
"languages": {
"title": "Мови"
}
}
},
"extractImages": {
"tags": "зображення,фото,збереження,архів,zip,захоплення,захоплення",
@@ -938,7 +1022,13 @@
"title": "Згладжування",
"header": "Згладжування PDF",
"flattenOnlyForms": "Згладити тільки форми",
"submit": "Згладити"
"submit": "Згладити",
"steps": {
"settings": "Налаштування"
},
"options": {
"flattenOnlyForms": "Згладити тільки форми"
}
},
"repair": {
"tags": "виправити,відновити,виправити,відновити",
@@ -1174,7 +1264,22 @@
"showLayers": "Показати шари (подвійне клацання для скидання всіх шарів до стану за умовчанням)",
"colourPicker": "Вибір кольору",
"findCurrentOutlineItem": "Знайти поточний елемент структури",
"applyChanges": "Застосувати зміни"
"applyChanges": "Застосувати зміни",
"auto": {
"settings": {
"advancedTitle": "Додаткове"
},
"wordsToRedact": {
"add": "Додати"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "Сторінки",
"placeholder": "(наприклад 1,2,8 або 4,7,12-16 або 2n-1)"
},
"export": "Експорт"
}
},
"tableExtraxt": {
"tags": "csv,видобуток таблиці,вилучення,конвертація"
@@ -1386,7 +1491,10 @@
"4": "Автоматичний режим - автоматично налаштовує якість для отримання PDF точного розміру",
"5": "Очікуваний розмір PDF (наприклад, 25 МБ, 10,8 МБ, 25 КБ)"
},
"submit": "Стиснути"
"submit": "Стиснути",
"method": {
"filesize": "Розмір файлу"
}
},
"decrypt": {
"passwordPrompt": "Цей файл захищений паролем. Будь ласка, введіть пароль:",
@@ -1557,5 +1665,83 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Завантажити",
"undo": "Скасувати",
"convert": {
"title": "Конвертувати",
"settings": "Налаштування",
"color": "Колір",
"greyscale": "Відтінки сірого",
"fillPage": "Заповнення сторінки",
"pdfaDigitalSignatureWarning": "Цей PDF документ має цифровий підпис. Цей підпис буде видалений у наступному кроці.",
"grayscale": "Відтінки сірого"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"rightRail": {
"selectAll": "Вибрати все",
"deselectAll": "Скасувати вибір усіх"
},
"quickAccess": {
"sign": "Підпис"
},
"fileUpload": {
"loading": "Завантаження...",
"or": "або"
},
"fileManager": {
"fileName": "Ім'я",
"fileVersion": "Версія",
"selectAll": "Вибрати все",
"deselectAll": "Скасувати вибір усіх",
"deleteSelected": "Видалити вибрані",
"download": "Завантажити",
"delete": "Видалити"
},
"sanitize": {
"submit": "Дезінфекція PDF",
"steps": {
"settings": "Налаштування"
}
},
"changePermissions": {
"title": "Змінити дозволи",
"submit": "Змінити дозволи",
"permissions": {
"preventAssembly": {
"label": "Запобігти збірці документа"
},
"preventExtractContent": {
"label": "Запобігти вилученню контенту"
},
"preventExtractForAccessibility": {
"label": "Запобігти вилученню для доступності"
},
"preventFillInForm": {
"label": "Заборонити заповнення форм"
},
"preventModify": {
"label": "Запобігти модифікації"
},
"preventModifyAnnotations": {
"label": "Заборонити модифікацію анотацій"
},
"preventPrinting": {
"label": "Заборонити друк"
},
"preventPrintingFaithful": {
"label": "Заборонити друк різних форматів"
}
},
"tooltip": {
"header": {
"title": "Змінити дозволи"
}
}
}
}
}
+190 -21
View File
@@ -137,7 +137,7 @@
"scanHelp": "Trợ giúp quét thư mục",
"deletePrompt": "Bạn có chắc chắn muốn xóa pipeline",
"tags": "tự động hóa,chuỗi,kịch bản,xử lý hàng loạt",
"title": "Pipeline"
"title": "Pipeline (Nâng cao)"
},
"pipelineOptions": {
"header": "Cấu hình Pipeline",
@@ -578,6 +578,38 @@
"replace-color": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"convert": {
"title": "Chuyển đổi"
},
"attachments": {
"title": "Add attachments"
},
"extractPages": {
"title": "Trích xuất trang"
},
"removePages": {
"title": "Xóa",
"desc": "Xóa các trang không mong muốn khỏi tài liệu PDF của bạn."
},
"removeImagePdf": {
"title": "Remove image",
"desc": "Remove image from PDF to reduce file size"
},
"autoSizeSplitPDF": {
"title": "Tự động chia theo kích thước/số lượng",
"desc": "Chia một tệp PDF thành nhiều tài liệu dựa trên kích thước, số trang hoặc số lượng tài liệu"
},
"adjust-contrast": {
"title": "Điều chỉnh màu sắc/tương phản",
"desc": "Điều chỉnh độ tương phản, độ bão hòa và độ sáng của PDF"
},
"replaceColorPdf": {
"title": "Replace and Invert Color",
"desc": "Replace color for text and background in PDF and invert full color of pdf to reduce file size"
},
"changePermissions": {
"title": "Thay đổi quyền"
}
},
"viewPdf": {
@@ -602,10 +634,10 @@
"addFile": "Add File",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
"split": "Split",
"split": "Tách",
"moveLeft": "Move Left",
"moveRight": "Move Right",
"delete": "Delete",
"delete": "Xóa",
"dragDropMessage": "Page(s) Selected",
"undo": "Undo",
"redo": "Redo"
@@ -617,7 +649,10 @@
"sortByName": "Sắp xếp theo tên",
"sortByDate": "Sắp xếp theo ngày",
"removeCertSign": "Xóa chữ ký số trong tệp đã trộn?",
"submit": "Trộn"
"submit": "Trộn",
"sortBy": {
"filename": "Tên tệp"
}
},
"split": {
"tags": "Thao tác trang,chia,Nhiều trang,cắt,phía máy chủ",
@@ -634,7 +669,20 @@
"8": "Tài liệu #6: Trang 10"
},
"splitPages": "Nhập các trang cần chia:",
"submit": "Chia"
"submit": "Chia",
"steps": {
"settings": "Cài đặt"
},
"methods": {
"bySize": {
"name": "Kích thước tệp"
}
},
"value": {
"fileSize": {
"label": "Kích thước tệp"
}
}
},
"rotate": {
"tags": "phía máy chủ",
@@ -712,6 +760,9 @@
"type": {
"1": "Văn bản",
"2": "Hình ảnh"
},
"watermarkType": {
"text": "Văn bản"
}
},
"permissions": {
@@ -734,7 +785,9 @@
"submit": "Thay đổi"
},
"removePages": {
"tags": "Xóa trang,xóa trang"
"tags": "Xóa trang,xóa trang",
"title": "Xóa",
"submit": "Xóa"
},
"addPassword": {
"tags": "bảo mật,an toàn",
@@ -758,7 +811,12 @@
"15": "Hạn chế những gì có thể làm với tài liệu sau khi mở (Không được hỗ trợ bởi tất cả các trình đọc)",
"16": "Hạn chế việc mở tài liệu"
},
"submit": "Mã hóa"
"submit": "Mã hóa",
"tooltip": {
"permissions": {
"title": "Thay đổi quyền"
}
}
},
"removePassword": {
"tags": "bảo mật,Giải mã,an toàn,bỏ mật khẩu,xóa mật khẩu",
@@ -768,7 +826,12 @@
"1": "Chọn PDF để giải mã",
"2": "Mật khẩu"
},
"submit": "Xóa"
"submit": "Xóa",
"desc": "Xóa bảo vệ mật khẩu khỏi tài liệu PDF của bạn.",
"password": {
"stepTitle": "Xóa mật khẩu",
"label": "Mật khẩu hiện tại"
}
},
"compressPdfs": {
"tags": "ép,nhỏ,nhỏ gọn"
@@ -829,7 +892,25 @@
},
"help": "Vui lòng đọc tài liệu này về cách sử dụng cho các ngôn ngữ khác và/hoặc sử dụng không trong docker",
"credit": "Dịch vụ này sử dụng qpdf và Tesseract cho OCR.",
"submit": "Xử lý PDF với OCR"
"submit": "Xử lý PDF với OCR",
"desc": "Dọn dẹp bản quét và phát hiện văn bản từ hình ảnh trong PDF và thêm lại dưới dạng văn bản.",
"settings": {
"title": "Cài đặt",
"ocrMode": {
"label": "Chế độ OCR"
},
"languages": {
"label": "Ngôn ngữ"
}
},
"tooltip": {
"mode": {
"title": "Chế độ OCR"
},
"languages": {
"title": "Ngôn ngữ"
}
}
},
"extractImages": {
"tags": "hình ảnh,ảnh,lưu,lưu trữ,zip,chụp,lấy",
@@ -924,7 +1005,7 @@
"sharedSigs": "Shared Signatures",
"noSavedSigs": "No saved signatures found",
"addToAll": "Add to all pages",
"delete": "Delete",
"delete": "Xóa",
"first": "First page",
"last": "Last page",
"next": "Next page",
@@ -938,7 +1019,13 @@
"title": "Làm phẳng",
"header": "Làm phẳng PDF",
"flattenOnlyForms": "Chỉ làm phẳng biểu mẫu",
"submit": "Làm phẳng"
"submit": "Làm phẳng",
"steps": {
"settings": "Cài đặt"
},
"options": {
"flattenOnlyForms": "Chỉ làm phẳng biểu mẫu"
}
},
"repair": {
"tags": "sửa,khôi phục,sửa chữa,phục hồi",
@@ -1106,10 +1193,10 @@
"credit": "Sử dụng WeasyPrint"
},
"PDFToMarkdown": {
"tags": "markup,web-content,transformation,convert,md",
"tags": "đánh dấu,nội dung web,chuyển đổi,chuyển",
"title": "PDF To Markdown",
"header": "PDF To Markdown",
"submit": "Convert"
"submit": "Chuyển đổi"
},
"getPdfInfo": {
"tags": "thông tin,dữ liệu,số liệu thống kê,thống kê",
@@ -1148,7 +1235,7 @@
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction",
"submit": "Redact",
"submit": "Manual Redaction",
"textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@@ -1174,7 +1261,20 @@
"showLayers": "Show Layers (double-click to reset all layers to the default state)",
"colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes"
"applyChanges": "Apply Changes",
"auto": {
"settings": {
"advancedTitle": "Nâng cao"
},
"wordsToRedact": {
"add": "Thêm"
}
},
"manual": {
"pageRedactionNumbers": {
"placeholder": "(ví dụ: 1,2,8 hoặc 4,7,12-16 hoặc 2n-1)"
}
}
},
"tableExtraxt": {
"tags": "CSV,Trích xuất bảng,trích xuất,chuyển đổi"
@@ -1261,8 +1361,8 @@
},
"signer": "Signer",
"date": "Date",
"reason": "Reason",
"location": "Location",
"reason": "Lý do",
"location": "Vị trí",
"noSignatures": "No digital signatures found in this document",
"chain": {
"invalid": "Certificate chain validation failed - cannot verify signer's identity"
@@ -1281,7 +1381,7 @@
"validUntil": "Valid Until",
"algorithm": "Algorithm",
"keySize": "Key Size",
"version": "Version",
"version": "Phiên bản",
"keyUsage": "Key Usage",
"selfSigned": "Self-Signed",
"bits": "bits"
@@ -1386,7 +1486,10 @@
"4": "Chế độ tự động - Tự động điều chỉnh chất lượng để đạt được kích thước PDF chính xác",
"5": "Kích thước PDF mong muốn (ví dụ: 25MB, 10.8MB, 25KB)"
},
"submit": "Nén"
"submit": "Nén",
"method": {
"filesize": "Kích thước tệp"
}
},
"decrypt": {
"passwordPrompt": "This file is password-protected. Please enter the password:",
@@ -1501,7 +1604,7 @@
"3": "Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.",
"4": "Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs."
},
"submit": "Split PDF"
"submit": "Chia PDF"
},
"fileChooser": {
"click": "Click",
@@ -1557,5 +1660,71 @@
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
}
}
},
"download": "Tải xuống",
"convert": {
"title": "Chuyển đổi",
"settings": "Cài đặt",
"color": "Màu sắc",
"greyscale": "Thang độ xám",
"fillPage": "Lấp đầy trang",
"pdfaDigitalSignatureWarning": "PDF chứa chữ ký số. Điều này sẽ bị xóa trong bước tiếp theo.",
"grayscale": "Thang độ xám"
},
"attachments": {
"tags": "embed,attach,file,attachment,attachments",
"title": "Add attachments",
"header": "Add attachments",
"submit": "Add attachments"
},
"quickAccess": {
"sign": "Ký"
},
"fileManager": {
"fileName": "Tên",
"fileVersion": "Phiên bản",
"download": "Tải xuống",
"delete": "Xóa"
},
"sanitize": {
"submit": "Làm sạch PDF",
"steps": {
"settings": "Cài đặt"
}
},
"changePermissions": {
"title": "Thay đổi quyền",
"submit": "Thay đổi quyền",
"permissions": {
"preventAssembly": {
"label": "Ngăn chặn lắp ráp tài liệu"
},
"preventExtractContent": {
"label": "Ngăn chặn trích xuất nội dung"
},
"preventExtractForAccessibility": {
"label": "Ngăn chặn trích xuất để truy cập"
},
"preventFillInForm": {
"label": "Ngăn chặn điền vào biểu mẫu"
},
"preventModify": {
"label": "Ngăn chặn sửa đổi"
},
"preventModifyAnnotations": {
"label": "Ngăn chặn sửa đổi chú thích"
},
"preventPrinting": {
"label": "Ngăn chặn in"
},
"preventPrintingFaithful": {
"label": "Ngăn chặn in các định dạng khác nhau"
}
},
"tooltip": {
"header": {
"title": "Thay đổi quyền"
}
}
}
}
}
+274 -83
View File
@@ -25,9 +25,9 @@
"multiPdfDropPrompt": "选择(或拖拽)所需的 PDF",
"imgPrompt": "选择图像",
"genericSubmit": "提交",
"uploadLimit": "Maximum file size:",
"uploadLimitExceededSingular": "is too large. Maximum allowed size is",
"uploadLimitExceededPlural": "are too large. Maximum allowed size is",
"uploadLimit": "最大文件大小:",
"uploadLimitExceededSingular": "文件过大。最大允许大小为",
"uploadLimitExceededPlural": "文件过大。最大允许大小为",
"processTimeWarning": "警告:此过程可能需要多达一分钟,具体时间取决于文件大小",
"pageOrderPrompt": "页面顺序(输入逗号分隔的页码列表或函数):",
"pageSelectionPrompt": "自定义页面选择(输入以逗号分隔的页码列表或函数:1,5,6、2n+1):",
@@ -118,14 +118,14 @@
"addToDoc": "添加至文件",
"reset": "重置",
"apply": "应用",
"noFileSelected": "No file selected. Please upload one.",
"noFileSelected": "未选择文件,请上传一个文件。",
"legal": {
"privacy": "隐私政策",
"terms": "服务条款",
"accessibility": "无障碍",
"cookie": "Cookie 政策",
"impressum": "Impressum",
"showCookieBanner": "Cookie Preferences"
"impressum": "版权声明",
"showCookieBanner": "Cookie 偏好设置"
},
"pipeline": {
"header": "流水线菜单(Beta)",
@@ -265,32 +265,32 @@
"disabledUsers": "禁用用户:",
"totalUsers": "总用户:",
"lastRequest": "最后登录",
"usage": "View Usage"
"usage": "查看使用情况"
},
"endpointStatistics": {
"title": "Endpoint Statistics",
"header": "Endpoint Statistics",
"top10": "Top 10",
"top20": "Top 20",
"all": "All",
"refresh": "Refresh",
"includeHomepage": "Include Homepage ('/')",
"includeLoginPage": "Include Login Page ('/login')",
"totalEndpoints": "Total Endpoints",
"totalVisits": "Total Visits",
"showing": "Showing",
"selectedVisits": "Selected Visits",
"endpoint": "Endpoint",
"visits": "Visits",
"percentage": "Percentage",
"loading": "Loading...",
"failedToLoad": "Failed to load endpoint data. Please try refreshing.",
"home": "Home",
"login": "Login",
"top": "Top",
"numberOfVisits": "Number of Visits",
"visitsTooltip": "Visits: {0} ({1}% of total)",
"retry": "Retry"
"title": "端点统计",
"header": "端点统计",
"top10": "前10",
"top20": "前20",
"all": "全部",
"refresh": "刷新",
"includeHomepage": "包含主页('/')",
"includeLoginPage": "包含登录页('/login')",
"totalEndpoints": "端点总数",
"totalVisits": "访问总数",
"showing": "显示",
"selectedVisits": "选中访问数",
"endpoint": "端点",
"visits": "访问次数",
"percentage": "百分比",
"loading": "加载中...",
"failedToLoad": "加载端点数据失败。请尝试刷新。",
"home": "主页",
"login": "登录",
"top": "顶部",
"numberOfVisits": "访问次数",
"visitsTooltip": "访问次数:{0}(占总数的{1}%)",
"retry": "重试"
},
"database": {
"title": "数据库 导入/导出",
@@ -320,7 +320,7 @@
"desc": "本地部署的一站式服务,满足您的所有 PDF 需求。",
"searchBar": "搜索您需要的功能...",
"viewPdf": {
"title": "View/Edit PDF",
"title": "浏览/编辑 PDF",
"desc": "浏览、注释、添加文本或图像"
},
"setFavorites": "编辑收藏夹",
@@ -388,8 +388,8 @@
"desc": "压缩 PDF 文件以减小文件大小。"
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "解锁PDF表单",
"desc": "移除表单字段只读属性"
},
"changeMetadata": {
"title": "更改元数据",
@@ -578,11 +578,48 @@
"replace-color": {
"title": "替换和反转颜色",
"desc": "替换 PDF 中文本和背景的颜色,并将PDF全色反转以减小文件大小"
},
"convert": {
"title": "转换"
},
"attachments": {
"title": "添加附件",
"desc": "在 PDF 中添加或删除嵌入文件(附件)"
},
"editTableOfContents": {
"title": "编辑目录",
"desc": "为 PDF 文档添加或编辑目录和书签"
},
"extractPages": {
"title": "提取页面"
},
"removePages": {
"title": "删除",
"desc": "从 PDF 文档中删除不需要的页面。"
},
"removeImagePdf": {
"title": "删除图像",
"desc": "删除图像减少 PDF 大小"
},
"autoSizeSplitPDF": {
"title": "自动根据大小/数目拆分 PDF",
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数"
},
"adjust-contrast": {
"title": "调整颜色/对比度",
"desc": "调整 PDF 的对比度、饱和度和亮度"
},
"replaceColorPdf": {
"title": "替换和反转颜色",
"desc": "替换 PDF 中文本和背景的颜色,并将PDF反转颜色以减小文件大小"
},
"changePermissions": {
"title": "更改权限"
}
},
"viewPdf": {
"tags": "浏览、阅读、注释、文本、图像",
"title": "View/Edit PDF",
"title": "浏览/编辑 PDF",
"header": "浏览 PDF"
},
"multiTool": {
@@ -592,9 +629,9 @@
"uploadPrompts": "文件名",
"selectAll": "选择所有",
"deselectAll": "取消选择所有",
"selectPages": "Page Select",
"selectPages": "页面选择",
"selectedPages": "已选择的页面",
"page": "Page",
"page": "页面",
"deleteSelected": "删除已选",
"downloadAll": "导出全部",
"downloadSelected": "导出已选",
@@ -617,7 +654,10 @@
"sortByName": "按名称排序",
"sortByDate": "按日期排序",
"removeCertSign": "删除合并文件的数字签名吗?",
"submit": "合并"
"submit": "合并",
"sortBy": {
"filename": "文件名"
}
},
"split": {
"tags": "页面操作,划分,多页面,剪切,服务器端",
@@ -634,7 +674,20 @@
"8": "文档 #6:第 9 页和第 10 页"
},
"splitPages": "输入要分割的页面:",
"submit": "拆分"
"submit": "拆分",
"steps": {
"settings": "设置"
},
"methods": {
"bySize": {
"name": "文件大小"
}
},
"value": {
"fileSize": {
"label": "文件大小"
}
}
},
"rotate": {
"tags": "服务器端",
@@ -679,7 +732,7 @@
"8": "删除最后一页",
"9": "删除第一页和最后一页",
"10": "奇偶合并",
"11": "Duplicate all pages"
"11": "复制所有页面"
},
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1)"
},
@@ -712,6 +765,12 @@
"type": {
"1": "文字",
"2": "图片"
},
"watermarkType": {
"text": "文本"
},
"settings": {
"fontSize": "字体大小"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "改变"
},
"removePages": {
"tags": "删除页面、删除"
"tags": "删除页面、删除",
"title": "删除",
"submit": "删除"
},
"addPassword": {
"tags": "安全、密码、加密",
@@ -758,7 +819,12 @@
"15": "限制打开后对文档的操作(不被所有阅读器支持)",
"16": "限制打开文档本身"
},
"submit": "加密"
"submit": "加密",
"tooltip": {
"permissions": {
"title": "更改权限"
}
}
},
"removePassword": {
"tags": "安全、解密、密码、安全性、删除密码",
@@ -768,15 +834,20 @@
"1": "选择要解密的 PDF",
"2": "密码"
},
"submit": "删除"
"submit": "删除",
"desc": "从 PDF 文档中移除密码保护。",
"password": {
"stepTitle": "删除密码",
"label": "当前密码"
}
},
"compressPdfs": {
"tags": "压缩、小、微小"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"tags": "移除,删除,表单,字段,只读",
"title": "移除表单字段只读属性",
"header": "解锁 PDF 表单",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +900,25 @@
},
"help": "请阅读此文档,了解如何将其用于其他语言和/或不在 docker 中使用。",
"credit": "此服务使用 qpdf 和 Tesseract 进行 OCR。",
"submit": "用 OCR 处理 PDF"
"submit": "用 OCR 处理 PDF",
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。",
"settings": {
"title": "设置",
"ocrMode": {
"label": "OCR 模式"
},
"languages": {
"label": "语言"
}
},
"tooltip": {
"mode": {
"title": "OCR 模式"
},
"languages": {
"title": "语言"
}
}
},
"extractImages": {
"tags": "图片、照片、保存、归档、压缩包、截取、抓取",
@@ -938,7 +1027,13 @@
"title": "展平",
"header": "展平 PDF",
"flattenOnlyForms": "仅展平表格",
"submit": "展平"
"submit": "展平",
"steps": {
"settings": "设置"
},
"options": {
"flattenOnlyForms": "仅展平表格"
}
},
"repair": {
"tags": "修复、恢复、纠正、恢复",
@@ -1174,7 +1269,22 @@
"showLayers": "显示图层(双击将所有图层重置为默认状态)",
"colourPicker": "颜色选择器",
"findCurrentOutlineItem": "查找当前大纲项目",
"applyChanges": "应用"
"applyChanges": "应用",
"auto": {
"settings": {
"advancedTitle": "高级功能"
},
"wordsToRedact": {
"add": "添加"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "页码",
"placeholder": "(例如:1,2,8 或 4,7,12-16 或 2n-1)"
},
"export": "导出全部"
}
},
"tableExtraxt": {
"tags": "CSV、表格提取、提取、转换"
@@ -1331,12 +1441,12 @@
"oauth2AccessDenied": "拒绝访问",
"oauth2InvalidTokenResponse": "无效的 Token 响应",
"oauth2InvalidIdToken": "无效的 Token",
"relyingPartyRegistrationNotFound": "No relying party registration found",
"relyingPartyRegistrationNotFound": "未找到依赖方注册信息",
"userIsDisabled": "用户被禁用,登录已被阻止。请联系管理员。",
"alreadyLoggedIn": "您已经登录到了",
"alreadyLoggedIn2": "设备,请注销设备后重试。",
"toManySessions": "你已经有太多的会话了。请注销一些设备后重试。",
"logoutMessage": "You have been logged out."
"logoutMessage": "您已退出登录。"
},
"pdfToSinglePage": {
"title": "PDF 转单页",
@@ -1355,10 +1465,10 @@
"selectText": {
"1": "移除 JavaScript 操作",
"2": "移除嵌入的文件",
"3": "Remove XMP metadata",
"3": "移除 XMP 元数据",
"4": "移除链接",
"5": "移除字体",
"6": "Remove Document Info Metadata"
"6": "移除文档信息元数据"
},
"submit": "清理PDF"
},
@@ -1380,13 +1490,16 @@
"selectText": {
"1": {
"_value": "Compression Settings",
"1": "1-3 PDF compression,</br> 4-6 lite image compression,</br> 7-9 intense image compression Will dramatically reduce image quality"
"1": "1-3 PDF 压缩,</br> 4-6 轻度图像压缩,</br> 7-9 深度图像压缩(将显著降低图像质量)"
},
"2": "优化级别:",
"4": "自动模式 - 自动调整质量以获得精确大小的PDF",
"5": "预期PDF大小(例如:25MB、10.8MB、25KB)"
},
"submit": "压缩"
"submit": "压缩",
"method": {
"filesize": "文件大小"
}
},
"decrypt": {
"passwordPrompt": "此文件受密码保护。请输入密码:",
@@ -1472,15 +1585,15 @@
"button": "参与调查",
"dontShowAgain": "不再显示",
"meeting": {
"1": "If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session.",
"2": "This is a chance to:",
"3": "Get help with deployment, integrations, or troubleshooting",
"4": "Provide direct feedback on performance, edge cases, and feature gaps",
"5": "Help us refine Stirling PDF for real-world enterprise use",
"6": "If you're interested, you can book time with our team directly. (English speaking only)",
"7": "Looking forward to digging into your use cases and making Stirling PDF even better!",
"notInterested": "Not a business and/or interested in a meeting?",
"button": "Book meeting"
"1": "如果您在工作中使用 Stirling PDF,我们非常希望与您交流。我们正在提供技术支持服务,以换取一次 15 分钟的用户访谈。",
"2": "这是一个机会:",
"3": "获取部署、集成或故障排除方面的帮助",
"4": "提供直接反馈,包括性能、边缘案例和功能差距",
"5": "帮助我们改进 Stirling PDF 以满足实际的企业使用需求",
"6": "如果您有兴趣,可以直接与我们团队预约时间。(仅限英语)",
"7": "期待深入了解您的使用案例,并使 Stirling PDF 变得更好!",
"notInterested": "不是企业或对会议不感兴趣?",
"button": "预约会议"
}
},
"removeImage": {
@@ -1523,39 +1636,117 @@
},
"cookieBanner": {
"popUp": {
"title": "How we use Cookies",
"title": "我们如何使用 Cookie",
"description": {
"1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.",
"1": "我们使用 Cookie 和其他技术来使 Stirling PDF 更好地为您服务——帮助我们改进工具并构建您喜爱的功能。",
"2": "If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
},
"acceptAllBtn": "Okay",
"acceptNecessaryBtn": "No Thanks",
"showPreferencesBtn": "Manage preferences"
"acceptAllBtn": "全部接受",
"acceptNecessaryBtn": "拒绝",
"showPreferencesBtn": "管理偏好设置"
},
"preferencesModal": {
"title": "Consent Preferences Center",
"acceptAllBtn": "Accept all",
"acceptNecessaryBtn": "Reject all",
"savePreferencesBtn": "Save preferences",
"closeIconLabel": "Close modal",
"serviceCounterLabel": "Service|Services",
"subtitle": "Cookie Usage",
"title": "隐私偏好设置中心",
"acceptAllBtn": "全部接受",
"acceptNecessaryBtn": "拒绝所有",
"savePreferencesBtn": "保存设置",
"closeIconLabel": "关闭弹窗",
"serviceCounterLabel": "服务",
"subtitle": "Cookie使用说明",
"description": {
"1": "Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.",
"2": "Stirling PDF cannot—and will never—track or access the content of the documents you use.",
"3": "Your privacy and trust are at the core of what we do."
"1": "Stirling PDF通过Cookie及类似技术优化用户体验并分析工具使用情况,帮助我们提升性能、开发实用功能并提供持续支持。",
"2": "我们承诺:Stirling PDF永远不会追踪或访问您使用的文档内容。",
"3": "用户隐私与信任是我们一切工作的核心。"
},
"necessary": {
"title": {
"1": "Strictly Necessary Cookies",
"2": "Always Enabled"
"1": "必要Cookie",
"2": "始终启用"
},
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off."
},
"analytics": {
"title": "Analytics",
"description": "These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with."
"title": "分析统计",
"description": "这些Cookie帮助我们分析工具使用情况,以便聚焦开发用户最需要的功能。再次强调:Stirling PDF绝不会追踪您处理的文档内容。"
}
}
},
"download": "下载",
"undo": "撤销",
"convert": {
"title": "转换",
"settings": "设置",
"color": "颜色",
"greyscale": "灰度",
"fillPage": "填充页面",
"pdfaDigitalSignatureWarning": "该PDF包含数字签名,下一步将移除该签名。",
"grayscale": "灰度"
},
"attachments": {
"tags": "嵌入、附件、文件、附加",
"title": "添加附件",
"header": "添加附件",
"submit": "添加附件"
},
"rightRail": {
"selectAll": "选择所有",
"deselectAll": "取消选择所有"
},
"quickAccess": {
"sign": "签名"
},
"fileUpload": {
"loading": "加载中...",
"or": "或"
},
"fileManager": {
"fileName": "名称",
"fileVersion": "版本",
"selectAll": "选择所有",
"deselectAll": "取消选择所有",
"deleteSelected": "删除已选",
"download": "下载",
"delete": "删除"
},
"sanitize": {
"submit": "清理 PDF",
"steps": {
"settings": "设置"
}
},
"changePermissions": {
"title": "更改权限",
"submit": "更改权限",
"permissions": {
"preventAssembly": {
"label": "防止文件的拼接"
},
"preventExtractContent": {
"label": "防止内容提取"
},
"preventExtractForAccessibility": {
"label": "防止提取内容的可访问性"
},
"preventFillInForm": {
"label": "防止填写表格"
},
"preventModify": {
"label": "防止修改"
},
"preventModifyAnnotations": {
"label": "防止修改注释"
},
"preventPrinting": {
"label": "防止打印"
},
"preventPrintingFaithful": {
"label": "防止打印不同的格式"
}
},
"tooltip": {
"header": {
"title": "更改权限"
}
}
}
}
}
+207 -16
View File
@@ -388,8 +388,8 @@
"desc": "壓縮 PDF 以減少其檔案大小。"
},
"unlockPDFForms": {
"title": "Unlock PDF Forms",
"desc": "Remove read-only property of form fields in a PDF document."
"title": "解鎖 PDF 表單",
"desc": "移除 PDF 文件中表單欄位的唯讀屬性"
},
"changeMetadata": {
"title": "變更中繼資料",
@@ -578,6 +578,43 @@
"replace-color": {
"title": "取代與反轉顏色",
"desc": "取代 PDF 中文字和背景的顏色,並反轉整個 PDF 的顏色以減少檔案大小"
},
"convert": {
"title": "轉換"
},
"attachments": {
"title": "新增附件",
"desc": "將檔案(附件)新增或移除至/從 PDF"
},
"editTableOfContents": {
"title": "編輯目錄",
"desc": "在 PDF 文件中新增或編輯書籤和目錄"
},
"extractPages": {
"title": "提取頁面"
},
"removePages": {
"title": "移除",
"desc": "從您的 PDF 檔案中刪除不需要的頁面。"
},
"removeImagePdf": {
"title": "移除圖片",
"desc": "從 PDF 中移除圖片以減少檔案大小"
},
"autoSizeSplitPDF": {
"title": "根據大小/數量自動分割",
"desc": "根據大小、頁數或文件數將單一 PDF 分割為多個文件"
},
"adjust-contrast": {
"title": "調整顏色/對比",
"desc": "調整 PDF 的對比、飽和度和亮度"
},
"replaceColorPdf": {
"title": "取代與反轉顏色",
"desc": "取代 PDF 中文字和背景的顏色,並反轉整個 PDF 的顏色以減少檔案大小"
},
"changePermissions": {
"title": "變更權限"
}
},
"viewPdf": {
@@ -617,7 +654,10 @@
"sortByName": "依名稱排序",
"sortByDate": "依日期排序",
"removeCertSign": "是否移除合併後檔案的憑證簽章?",
"submit": "合併"
"submit": "合併",
"sortBy": {
"filename": "檔案名稱"
}
},
"split": {
"tags": "頁面操作,劃分,多頁,剪下,伺服器端",
@@ -634,7 +674,20 @@
"8": "文件 #6:頁面 10"
},
"splitPages": "輸入要分割的頁面:",
"submit": "分割"
"submit": "分割",
"steps": {
"settings": "設定"
},
"methods": {
"bySize": {
"name": "檔案大小"
}
},
"value": {
"fileSize": {
"label": "檔案大小"
}
}
},
"rotate": {
"tags": "伺服器端",
@@ -712,6 +765,12 @@
"type": {
"1": "文字",
"2": "圖片"
},
"watermarkType": {
"text": "文字"
},
"settings": {
"fontSize": "字型大小"
}
},
"permissions": {
@@ -734,7 +793,9 @@
"submit": "變更"
},
"removePages": {
"tags": "移除頁面,刪除頁面"
"tags": "移除頁面,刪除頁面",
"title": "移除",
"submit": "移除"
},
"addPassword": {
"tags": "安全,安全性",
@@ -758,7 +819,12 @@
"15": "限制一旦開啟文件可以做什麼(並非所有軟體都支援)",
"16": "限制開啟文件本身"
},
"submit": "加密"
"submit": "加密",
"tooltip": {
"permissions": {
"title": "變更權限"
}
}
},
"removePassword": {
"tags": "安全,解密,安全性,取消密碼,刪除密碼",
@@ -768,15 +834,20 @@
"1": "選擇要解密的 PDF",
"2": "密碼"
},
"submit": "移除"
"submit": "移除",
"desc": "從您的 PDF 檔案中移除密碼保護。",
"password": {
"stepTitle": "移除密碼",
"label": "目前密碼"
}
},
"compressPdfs": {
"tags": "壓縮,小,微小"
},
"unlockPDFForms": {
"tags": "remove,delete,form,field,readonly",
"title": "Remove Read-Only from Form Fields",
"header": "Unlock PDF Forms",
"tags": "移除,刪除,表格,欄位,唯讀",
"title": "移除表單欄位的唯讀限制",
"header": "解鎖 PDF 表單",
"submit": "Remove"
},
"changeMetadata": {
@@ -829,7 +900,25 @@
},
"help": "請閱讀此文件,了解如何使用其他語言和/或在 Docker 中使用",
"credit": "此服務使用 qpdf 和 Tesseract 進行 OCR。",
"submit": "使用 OCR 處理 PDF"
"submit": "使用 OCR 處理 PDF",
"desc": "清理掃描並從 PDF 中的影像中偵測文字並重新新增為文字。",
"settings": {
"title": "設定",
"ocrMode": {
"label": "OCR 模式"
},
"languages": {
"label": "語言"
}
},
"tooltip": {
"mode": {
"title": "OCR 模式"
},
"languages": {
"title": "語言"
}
}
},
"extractImages": {
"tags": "圖片,照片,儲存,存檔,壓縮檔,捕獲,抓取",
@@ -938,7 +1027,13 @@
"title": "平坦化",
"header": "PDF 平坦化",
"flattenOnlyForms": "僅將表單平坦化",
"submit": "平坦化"
"submit": "平坦化",
"steps": {
"settings": "設定"
},
"options": {
"flattenOnlyForms": "僅將表單平坦化"
}
},
"repair": {
"tags": "修復,恢復,修正,復原",
@@ -1174,7 +1269,22 @@
"showLayers": "顯示圖層(按兩下可將所有圖層重設為預設狀態)",
"colourPicker": "顏色選擇器",
"findCurrentOutlineItem": "尋找目前的大綱項目",
"applyChanges": "套用變更"
"applyChanges": "套用變更",
"auto": {
"settings": {
"advancedTitle": "進階"
},
"wordsToRedact": {
"add": "新增"
}
},
"manual": {
"pageRedactionNumbers": {
"title": "頁面",
"placeholder": "(例如 1,2,8 或 4,7,12-16 或 2n-1)"
},
"export": "匯出"
}
},
"tableExtraxt": {
"tags": "CSV,表格提取,提取,轉換"
@@ -1336,7 +1446,7 @@
"alreadyLoggedIn": "您已經登入了",
"alreadyLoggedIn2": "部裝置。請先從這些裝置登出後再試一次。",
"toManySessions": "您有太多使用中的工作階段",
"logoutMessage": "You have been logged out."
"logoutMessage": "您已登出。"
},
"pdfToSinglePage": {
"title": "PDF 轉為單一頁面",
@@ -1386,7 +1496,10 @@
"4": "自動模式 - 自動調整品質使 PDF 達到指定的檔案大小",
"5": "指定的 PDF 檔案大小(例如 25MB, 10.8MB, 25KB)"
},
"submit": "壓縮"
"submit": "壓縮",
"method": {
"filesize": "檔案大小"
}
},
"decrypt": {
"passwordPrompt": "此檔案已受密碼保護。請輸入密碼:",
@@ -1557,5 +1670,83 @@
"description": "這些 Cookies 幫助我們分析您如何使用我們的工具,好讓我們能專注在構建社群最重視的功能。儘管放心—— Stirling PDF 不會且永不追蹤您的文件"
}
}
},
"download": "下載",
"undo": "撤銷",
"convert": {
"title": "轉換",
"settings": "設定",
"color": "顏色",
"greyscale": "灰度",
"fillPage": "填充頁面",
"pdfaDigitalSignatureWarning": "該 PDF 的憑證簽章將會在下一步被移除",
"grayscale": "灰度"
},
"attachments": {
"tags": "嵌入,附件,檔案,附加,附件管理",
"title": "新增附件",
"header": "新增附件",
"submit": "新增附件"
},
"rightRail": {
"selectAll": "全選",
"deselectAll": "取消全選"
},
"quickAccess": {
"sign": "簽章"
},
"fileUpload": {
"loading": "載入中...",
"or": "或"
},
"fileManager": {
"fileName": "名稱",
"fileVersion": "版本",
"selectAll": "全選",
"deselectAll": "取消全選",
"deleteSelected": "刪除已選取的項目",
"download": "下載",
"delete": "刪除"
},
"sanitize": {
"submit": "清理 PDF",
"steps": {
"settings": "設定"
}
},
"changePermissions": {
"title": "變更權限",
"submit": "變更權限",
"permissions": {
"preventAssembly": {
"label": "防止文件組裝"
},
"preventExtractContent": {
"label": "防止內容提取"
},
"preventExtractForAccessibility": {
"label": "防止為了無障礙使用而提取資料"
},
"preventFillInForm": {
"label": "防止填寫表單"
},
"preventModify": {
"label": "防止修改"
},
"preventModifyAnnotations": {
"label": "防止註釋修改"
},
"preventPrinting": {
"label": "防止列印"
},
"preventPrintingFaithful": {
"label": "防止列印不同格式"
}
},
"tooltip": {
"header": {
"title": "變更權限"
}
}
}
}
}
-157
View File
@@ -1,157 +0,0 @@
// Web Worker for parallel thumbnail generation
console.log('🔧 Thumbnail worker starting up...');
let pdfJsLoaded = false;
// Import PDF.js properly for worker context
try {
console.log('📦 Loading PDF.js locally...');
importScripts('/pdf.js');
// PDF.js exports to globalThis, check both self and globalThis
const pdfjsLib = self.pdfjsLib || globalThis.pdfjsLib;
if (pdfjsLib) {
// Make it available on self for consistency
self.pdfjsLib = pdfjsLib;
// Set up PDF.js worker
self.pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
pdfJsLoaded = true;
console.log('✓ PDF.js loaded successfully from local files');
console.log('✓ PDF.js version:', self.pdfjsLib.version || 'unknown');
} else {
throw new Error('pdfjsLib not available after import - neither self.pdfjsLib nor globalThis.pdfjsLib found');
}
} catch (error) {
console.error('✗ Failed to load local PDF.js:', error.message || error);
console.error('✗ Available globals:', Object.keys(self).filter(key => key.includes('pdf')));
pdfJsLoaded = false;
}
// Log the final status
if (pdfJsLoaded) {
console.log('✅ Thumbnail worker ready for PDF processing');
} else {
console.log('❌ Thumbnail worker failed to initialize - PDF.js not available');
}
self.onmessage = async function(e) {
const { type, data, jobId } = e.data;
try {
// Handle PING for worker health check
if (type === 'PING') {
console.log('🏓 Worker PING received, checking PDF.js status...');
// Check if PDF.js is loaded before responding
if (pdfJsLoaded && self.pdfjsLib) {
console.log('✓ Worker PONG - PDF.js ready');
self.postMessage({ type: 'PONG', jobId });
} else {
console.error('✗ PDF.js not loaded - worker not ready');
console.error('✗ pdfJsLoaded:', pdfJsLoaded);
console.error('✗ self.pdfjsLib:', !!self.pdfjsLib);
self.postMessage({
type: 'ERROR',
jobId,
data: { error: 'PDF.js not loaded in worker' }
});
}
return;
}
if (type === 'GENERATE_THUMBNAILS') {
console.log('🖼️ Starting thumbnail generation for', data.pageNumbers.length, 'pages');
if (!pdfJsLoaded || !self.pdfjsLib) {
const error = 'PDF.js not available in worker';
console.error('✗', error);
throw new Error(error);
}
const { pdfArrayBuffer, pageNumbers, scale = 0.2, quality = 0.8 } = data;
console.log('📄 Loading PDF document, size:', pdfArrayBuffer.byteLength, 'bytes');
// Load PDF in worker using imported PDF.js
const pdf = await self.pdfjsLib.getDocument({ data: pdfArrayBuffer }).promise;
console.log('✓ PDF loaded, total pages:', pdf.numPages);
const thumbnails = [];
// Process pages in smaller batches for smoother UI
const batchSize = 3; // Process 3 pages at once for smoother UI
for (let i = 0; i < pageNumbers.length; i += batchSize) {
const batch = pageNumbers.slice(i, i + batchSize);
const batchPromises = batch.map(async (pageNumber) => {
try {
console.log(`🎯 Processing page ${pageNumber}...`);
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
console.log(`📐 Page ${pageNumber} viewport:`, viewport.width, 'x', viewport.height);
// Create OffscreenCanvas for better performance
const canvas = new OffscreenCanvas(viewport.width, viewport.height);
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Failed to get 2D context from OffscreenCanvas');
}
await page.render({ canvasContext: context, viewport }).promise;
console.log(`✓ Page ${pageNumber} rendered`);
// Convert to blob then to base64 (more efficient than toDataURL)
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const thumbnail = `data:image/jpeg;base64,${base64}`;
console.log(`✓ Page ${pageNumber} thumbnail generated (${base64.length} chars)`);
return { pageNumber, thumbnail, success: true };
} catch (error) {
console.error(`✗ Failed to generate thumbnail for page ${pageNumber}:`, error.message || error);
return { pageNumber, error: error.message || String(error), success: false };
}
});
const batchResults = await Promise.all(batchPromises);
thumbnails.push(...batchResults);
// Send progress update
console.log(`📊 Worker: Sending progress update - ${thumbnails.length}/${pageNumbers.length} completed, ${batchResults.filter(r => r.success).length} new thumbnails`);
self.postMessage({
type: 'PROGRESS',
jobId,
data: {
completed: thumbnails.length,
total: pageNumbers.length,
thumbnails: batchResults.filter(r => r.success)
}
});
// Small delay between batches to keep UI smooth
if (i + batchSize < pageNumbers.length) {
console.log(`⏸️ Worker: Pausing 100ms before next batch (${i + batchSize}/${pageNumbers.length})`);
await new Promise(resolve => setTimeout(resolve, 100)); // Increased to 100ms pause between batches for smoother scrolling
}
}
// Clean up
pdf.destroy();
self.postMessage({
type: 'COMPLETE',
jobId,
data: { thumbnails: thumbnails.filter(r => r.success) }
});
}
} catch (error) {
self.postMessage({
type: 'ERROR',
jobId,
data: { error: error.message }
});
}
};
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env node
const { icons } = require('@iconify-json/material-symbols');
const fs = require('fs');
const path = require('path');
// Check for verbose flag
const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
// Logging functions
const info = (message) => console.log(message);
const debug = (message) => {
if (isVerbose) {
console.log(message);
}
};
// Function to scan codebase for LocalIcon usage
function scanForUsedIcons() {
const usedIcons = new Set();
const srcDir = path.join(__dirname, '..', 'src');
info('🔍 Scanning codebase for LocalIcon usage...');
if (!fs.existsSync(srcDir)) {
console.error('❌ Source directory not found:', srcDir);
process.exit(1);
}
// Recursively scan all .tsx and .ts files
function scanDirectory(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
scanDirectory(filePath);
} else if (file.endsWith('.tsx') || file.endsWith('.ts')) {
const content = fs.readFileSync(filePath, 'utf8');
// Match LocalIcon usage: <LocalIcon icon="icon-name" ...>
const localIconMatches = content.match(/<LocalIcon\s+[^>]*icon="([^"]+)"/g);
if (localIconMatches) {
localIconMatches.forEach(match => {
const iconMatch = match.match(/icon="([^"]+)"/);
if (iconMatch) {
usedIcons.add(iconMatch[1]);
debug(` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`);
}
});
}
// Match old material-symbols-rounded spans: <span className="material-symbols-rounded">icon-name</span>
const spanMatches = content.match(/<span[^>]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g);
if (spanMatches) {
spanMatches.forEach(match => {
const iconMatch = match.match(/>([^<]+)<\/span>/);
if (iconMatch && iconMatch[1].trim()) {
const iconName = iconMatch[1].trim();
usedIcons.add(iconName);
debug(` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`);
}
});
}
// Match Icon component usage: <Icon icon="material-symbols:icon-name" ...>
const iconMatches = content.match(/<Icon\s+[^>]*icon="material-symbols:([^"]+)"/g);
if (iconMatches) {
iconMatches.forEach(match => {
const iconMatch = match.match(/icon="material-symbols:([^"]+)"/);
if (iconMatch) {
usedIcons.add(iconMatch[1]);
debug(` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`);
}
});
}
}
});
}
scanDirectory(srcDir);
const iconArray = Array.from(usedIcons).sort();
info(`📋 Found ${iconArray.length} unique icons across codebase`);
return iconArray;
}
// Main async function
async function main() {
// Auto-detect used icons
const usedIcons = scanForUsedIcons();
// Check if we need to regenerate (compare with existing)
const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json');
let needsRegeneration = true;
if (fs.existsSync(outputPath)) {
try {
const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
const existingIcons = Object.keys(existingSet.icons || {}).sort();
const currentIcons = [...usedIcons].sort();
if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) {
needsRegeneration = false;
info(`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`);
}
} catch {
// If we can't parse existing file, regenerate
needsRegeneration = true;
}
}
if (!needsRegeneration) {
info('🎉 No regeneration needed!');
process.exit(0);
}
info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`);
// Dynamic import of ES module
const { getIcons } = await import('@iconify/utils');
// Extract only our used icons from the full set
const extractedIcons = getIcons(icons, usedIcons);
if (!extractedIcons) {
console.error('❌ Failed to extract icons');
process.exit(1);
}
// Check for missing icons
const extractedIconNames = Object.keys(extractedIcons.icons || {});
const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon));
if (missingIcons.length > 0) {
info(`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`);
info('💡 These icons don\'t exist in Material Symbols. Please use available alternatives.');
}
// Create output directory
const outputDir = path.join(__dirname, '..', 'src', 'assets');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write the extracted icon set to a file (outputPath already defined above)
fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2));
info(`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`);
info(`📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`);
info(`💾 Saved to: ${outputPath}`);
// Generate TypeScript types
const typesContent = `// Auto-generated icon types
// This file is automatically generated by scripts/generate-icons.js
// Do not edit manually - changes will be overwritten
export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')};
export interface IconSet {
prefix: string;
icons: Record<string, any>;
width?: number;
height?: number;
}
// Re-export the icon set as the default export with proper typing
declare const iconSet: IconSet;
export default iconSet;
`;
const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts');
fs.writeFileSync(typesPath, typesContent);
info(`📝 Generated types: ${typesPath}`);
info(`🎉 Icon extraction complete!`);
}
// Run the main function
main().catch(error => {
console.error('❌ Script failed:', error);
process.exit(1);
});
+58 -57
View File
@@ -24,17 +24,17 @@ try {
// Install license-checker if not present
try {
require.resolve('license-checker');
} catch (e) {
} catch {
console.log('📦 Installing license-checker...');
execSync('npm install --save-dev license-checker', { stdio: 'inherit' });
}
// Generate license report using license-checker (more reliable)
const licenseReport = execSync('npx license-checker --production --json', {
const licenseReport = execSync('npx license-checker --production --json', {
encoding: 'utf8',
cwd: path.dirname(PACKAGE_JSON)
});
let licenseData;
try {
licenseData = JSON.parse(licenseReport);
@@ -43,16 +43,16 @@ try {
console.error('Raw output:', licenseReport.substring(0, 500) + '...');
process.exit(1);
}
if (!licenseData || typeof licenseData !== 'object') {
console.error('❌ Invalid license data structure');
process.exit(1);
}
// Convert license-checker format to array
const licenseArray = Object.entries(licenseData).map(([key, value]) => {
let name, version;
// Handle scoped packages like @mantine/core@1.0.0
if (key.startsWith('@')) {
const parts = key.split('@');
@@ -64,30 +64,30 @@ try {
name = key.substring(0, lastAtIndex);
version = key.substring(lastAtIndex + 1);
}
// Normalize license types for edge cases
let licenseType = value.licenses;
// Handle missing or null licenses
if (!licenseType || licenseType === null || licenseType === undefined) {
licenseType = 'Unknown';
}
// Handle empty string licenses
if (licenseType === '') {
licenseType = 'Unknown';
}
// Handle array licenses (rare but possible)
if (Array.isArray(licenseType)) {
licenseType = licenseType.join(' AND ');
}
// Handle object licenses (fallback)
if (typeof licenseType === 'object' && licenseType !== null) {
licenseType = 'Unknown';
}
return {
name: name,
version: version || value.version || 'unknown',
@@ -97,13 +97,13 @@ try {
link: value.licenseUrl
};
});
// Transform to match Java backend format
const transformedData = {
dependencies: licenseArray.map(dep => {
const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown');
const licenseUrl = dep.link || getLicenseUrl(licenseType);
return {
moduleName: dep.name,
moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
@@ -113,29 +113,29 @@ try {
};
})
};
// Log summary of license types found
const licenseSummary = licenseArray.reduce((acc, dep) => {
const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown');
acc[license] = (acc[license] || 0) + 1;
return acc;
}, {});
console.log('📊 License types found:');
Object.entries(licenseSummary).forEach(([license, count]) => {
console.log(` ${license}: ${count} packages`);
});
// Log any complex or unusual license formats for debugging
const complexLicenses = licenseArray.filter(dep =>
const complexLicenses = licenseArray.filter(dep =>
dep.licenseType && (
dep.licenseType.includes('AND') ||
dep.licenseType.includes('OR') ||
dep.licenseType.includes('AND') ||
dep.licenseType.includes('OR') ||
dep.licenseType === 'Unknown' ||
dep.licenseType.includes('SEE LICENSE')
)
);
if (complexLicenses.length > 0) {
console.log('\n🔍 Complex/Edge case licenses detected:');
complexLicenses.forEach(dep => {
@@ -150,7 +150,7 @@ try {
problematicLicenses.forEach(warning => {
console.log(` ${warning.message}`);
});
// Write license warnings to a separate file for CI/CD
const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json');
fs.writeFileSync(warningsFile, JSON.stringify({
@@ -164,11 +164,11 @@ try {
// Write to file
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4));
console.log(`✅ License report generated successfully!`);
console.log(`📄 Found ${transformedData.dependencies.length} dependencies`);
console.log(`💾 Saved to: ${OUTPUT_FILE}`);
} catch (error) {
console.error('❌ Error generating license report:', error.message);
process.exit(1);
@@ -179,9 +179,10 @@ try {
*/
function getLicenseUrl(licenseType) {
if (!licenseType || licenseType === 'Unknown') return '';
const licenseUrls = {
'MIT': 'https://opensource.org/licenses/MIT',
'MIT*': 'https://opensource.org/licenses/MIT',
'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause',
@@ -206,12 +207,12 @@ function getLicenseUrl(licenseType) {
'Public Domain': 'https://creativecommons.org/publicdomain/zero/1.0/',
'UNLICENSED': ''
};
// Try exact match first
if (licenseUrls[licenseType]) {
return licenseUrls[licenseType];
}
// Try case-insensitive match
const lowerType = licenseType.toLowerCase();
for (const [key, url] of Object.entries(licenseUrls)) {
@@ -219,16 +220,16 @@ function getLicenseUrl(licenseType) {
return url;
}
}
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
if (licenseType.includes('AND') || licenseType.includes('OR')) {
// Extract the first license from compound expressions for URL
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-\.]+)/);
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
if (match && licenseUrls[match[1]]) {
return licenseUrls[match[1]];
}
}
// For non-standard licenses, return empty string (will use package link if available)
return '';
}
@@ -238,7 +239,7 @@ function getLicenseUrl(licenseType) {
*/
function checkLicenseCompatibility(licenseSummary, licenseArray) {
const warnings = [];
// Define problematic license patterns
const problematicLicenses = {
// Copyleft licenses
@@ -248,7 +249,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
'LGPL-3.0': 'Weak copyleft license - may require source disclosure for modifications',
'AGPL-3.0': 'Network copyleft license - requires source disclosure for network use',
'AGPL-1.0': 'Network copyleft license - requires source disclosure for network use',
// Other potentially problematic licenses
'WTFPL': 'Potentially problematic license - legal uncertainty',
'CC-BY-SA-4.0': 'ShareAlike license - requires derivative works to use same license',
@@ -267,47 +268,47 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
'UNLICENSED': 'No license specified - usage rights unclear',
'Unknown': 'License not detected - manual review required'
};
// Known good licenses (no warnings needed)
const goodLicenses = new Set([
'MIT', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0',
'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0',
'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE'
]);
// Helper function to normalize license names for comparison
function normalizeLicense(license) {
return license
.replace(/-or-later$/, '') // Remove -or-later suffix
.replace(/\+$/, '') // Remove + suffix
.replace(/\+$/, '') // Remove + suffix
.trim();
}
// Check each license type
Object.entries(licenseSummary).forEach(([license, count]) => {
// Skip known good licenses
if (goodLicenses.has(license)) {
return;
}
// Check if this license only affects our own packages
const affectedPackages = licenseArray.filter(dep => {
const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType;
return depLicense === license;
});
const isOnlyOurPackages = affectedPackages.every(dep =>
dep.name === 'frontend' ||
const isOnlyOurPackages = affectedPackages.every(dep =>
dep.name === 'frontend' ||
dep.name.toLowerCase().includes('stirling-pdf') ||
dep.name.toLowerCase().includes('stirling_pdf') ||
dep.name.toLowerCase().includes('stirlingpdf')
);
if (isOnlyOurPackages && (license === 'UNLICENSED' || license.startsWith('SEE LICENSE IN'))) {
return; // Skip warnings for our own Stirling-PDF packages
}
// Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
if (license.includes('AND') || license.includes('OR')) {
// For OR licenses, check if there's at least one acceptable license option
@@ -317,23 +318,23 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
.replace(/[()]/g, '') // Remove parentheses
.split(' OR ')
.map(component => component.trim());
// Check if any component is in the goodLicenses set (with normalization)
const hasGoodLicense = orComponents.some(component => {
const normalized = normalizeLicense(component);
return goodLicenses.has(component) || goodLicenses.has(normalized);
});
if (hasGoodLicense) {
return; // Skip warning - can use the good license option
}
}
// For AND licenses or OR licenses with no good options, check for problematic components
const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic =>
const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic =>
license.includes(problematic)
);
if (hasProblematicComponent) {
const affectedPackages = licenseArray
.filter(dep => {
@@ -345,12 +346,12 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
version: dep.version,
url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
}));
const licenseType = license.includes('AND') ? 'AND' : 'OR';
const reason = licenseType === 'AND'
const reason = licenseType === 'AND'
? 'Compound license with AND requirement - all components must be compatible'
: 'Compound license with potentially problematic components and no good fallback options';
warnings.push({
message: `📋 This PR contains ${count} package${count > 1 ? 's' : ''} with compound license "${license}" - manual review recommended`,
licenseType: license,
@@ -362,7 +363,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
}
return;
}
// Check for exact matches with problematic licenses
if (problematicLicenses[license]) {
const affectedPackages = licenseArray
@@ -375,10 +376,10 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
version: dep.version,
url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
}));
const packageList = affectedPackages.map(pkg => pkg.name).slice(0, 5).join(', ') + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : '');
const licenseUrl = getLicenseUrl(license) || 'https://opensource.org/licenses';
warnings.push({
message: `⚠️ This PR contains ${count} package${count > 1 ? 's' : ''} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`,
licenseType: license,
@@ -399,7 +400,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
version: dep.version,
url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
}));
warnings.push({
message: `❓ This PR contains ${count} package${count > 1 ? 's' : ''} with unknown license type "${license}" - manual review required`,
licenseType: license,
@@ -410,6 +411,6 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) {
});
}
});
return warnings;
}
}
+41 -20
View File
@@ -1,23 +1,34 @@
import React, { Suspense } from 'react';
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
import { FileContextProvider } from './contexts/FileContext';
import { FilesModalProvider } from './contexts/FilesModalContext';
import HomePage from './pages/HomePage';
import React, { 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 { SidebarProvider } from "./contexts/SidebarContext";
import ErrorBoundary from "./components/shared/ErrorBoundary";
import HomePage from "./pages/HomePage";
// Import global styles
import './styles/tailwind.css';
import './index.css';
import "./styles/tailwind.css";
import "./styles/cookieconsent.css";
import "./index.css";
import { RightRailProvider } from "./contexts/RightRailContext";
// Import file ID debugging helpers (development only)
import "./utils/fileIdSafety";
// Loading component for i18next suspense
const LoadingFallback = () => (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '18px',
color: '#666'
}}>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
fontSize: "18px",
color: "#666",
}}
>
Loading...
</div>
);
@@ -26,11 +37,21 @@ export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<RainbowThemeProvider>
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<FilesModalProvider>
<HomePage />
</FilesModalProvider>
</FileContextProvider>
<ErrorBoundary>
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<NavigationProvider>
<FilesModalProvider>
<ToolWorkflowProvider>
<SidebarProvider>
<RightRailProvider>
<HomePage />
</RightRailProvider>
</SidebarProvider>
</ToolWorkflowProvider>
</FilesModalProvider>
</NavigationProvider>
</FileContextProvider>
</ErrorBoundary>
</RainbowThemeProvider>
</Suspense>
);
+95 -11
View File
@@ -21,6 +21,13 @@
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "@atlaskit/pragmatic-drag-and-drop",
"moduleUrl": "https://github.com/atlassian/pragmatic-drag-and-drop",
"moduleVersion": "1.7.4",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "@babel/code-frame",
"moduleUrl": "https://github.com/babel/babel",
@@ -59,7 +66,7 @@
{
"moduleName": "@babel/parser",
"moduleUrl": "https://github.com/babel/babel",
"moduleVersion": "7.27.3",
"moduleVersion": "7.28.3",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
@@ -87,7 +94,7 @@
{
"moduleName": "@babel/types",
"moduleUrl": "https://github.com/babel/babel",
"moduleVersion": "7.27.3",
"moduleVersion": "7.28.2",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
@@ -217,6 +224,20 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@iconify/react",
"moduleUrl": "https://github.com/iconify/iconify",
"moduleVersion": "6.0.0",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@iconify/types",
"moduleUrl": "https://github.com/iconify/iconify",
"moduleVersion": "2.0.0",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@isaacs/fs-minipass",
"moduleUrl": "https://github.com/npm/fs-minipass",
@@ -364,6 +385,13 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@posthog/core",
"moduleUrl": "https://github.com/PostHog/posthog-js",
"moduleVersion": "1.0.2",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@tailwindcss/node",
"moduleUrl": "https://github.com/tailwindlabs/tailwindcss",
@@ -399,6 +427,20 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@tanstack/react-virtual",
"moduleUrl": "https://github.com/TanStack/virtual",
"moduleVersion": "3.13.12",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@tanstack/virtual-core",
"moduleUrl": "https://github.com/TanStack/virtual",
"moduleVersion": "3.13.12",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "@testing-library/dom",
"moduleUrl": "https://github.com/testing-library/dom-testing-library",
@@ -567,6 +609,13 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "bind-event-listener",
"moduleUrl": "https://github.com/alexreardon/bind-event-listener",
"moduleVersion": "3.0.0",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "brace-expansion",
"moduleUrl": "https://github.com/juliangruber/brace-expansion",
@@ -700,6 +749,13 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "core-js",
"moduleUrl": "https://github.com/zloirock/core-js",
"moduleVersion": "3.45.1",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "core-util-is",
"moduleUrl": "https://github.com/isaacs/core-util-is",
@@ -882,6 +938,13 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "fflate",
"moduleUrl": "https://github.com/101arrowz/fflate",
"moduleVersion": "0.4.8",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "file-selector",
"moduleUrl": "https://github.com/react-dropzone/file-selector",
@@ -1246,13 +1309,6 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "material-symbols",
"moduleUrl": "https://github.com/marella/material-symbols",
"moduleVersion": "0.33.0",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "math-intrinsics",
"moduleUrl": "https://github.com/es-shims/math-intrinsics",
@@ -1494,7 +1550,21 @@
{
"moduleName": "postcss",
"moduleUrl": "https://github.com/postcss/postcss",
"moduleVersion": "8.5.3",
"moduleVersion": "8.5.6",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "posthog-js",
"moduleUrl": "https://github.com/PostHog/posthog-js",
"moduleVersion": "1.261.0",
"moduleLicense": "MIT*",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "preact",
"moduleUrl": "https://github.com/preactjs/preact",
"moduleVersion": "10.27.1",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
@@ -1526,6 +1596,13 @@
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "raf-schd",
"moduleUrl": "https://github.com/alexreardon/raf-schd",
"moduleVersion": "4.0.3",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "react-dom",
"moduleUrl": "https://github.com/facebook/react",
@@ -1886,7 +1963,7 @@
{
"moduleName": "typescript",
"moduleUrl": "https://github.com/microsoft/TypeScript",
"moduleVersion": "5.8.3",
"moduleVersion": "5.9.2",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@@ -1953,6 +2030,13 @@
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "web-vitals",
"moduleUrl": "https://github.com/GoogleChrome/web-vitals",
"moduleVersion": "4.2.4",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "webidl-conversions",
"moduleUrl": "https://github.com/jsdom/webidl-conversions",
-315
View File
@@ -1,315 +0,0 @@
import { Command, CommandSequence } from '../hooks/useUndoRedo';
import { PDFDocument, PDFPage } from '../types/pageEditor';
// Base class for page operations
abstract class PageCommand implements Command {
protected pdfDocument: PDFDocument;
protected setPdfDocument: (doc: PDFDocument) => void;
protected previousState: PDFDocument;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void
) {
this.pdfDocument = pdfDocument;
this.setPdfDocument = setPdfDocument;
this.previousState = JSON.parse(JSON.stringify(pdfDocument)); // Deep clone
}
abstract execute(): void;
abstract description: string;
undo(): void {
this.setPdfDocument(this.previousState);
}
}
// Rotate pages command
export class RotatePagesCommand extends PageCommand {
private pageIds: string[];
private rotation: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[],
rotation: number
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.rotation = rotation;
}
execute(): void {
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
return { ...page, rotation: page.rotation + this.rotation };
}
return page;
});
this.setPdfDocument({ ...this.pdfDocument, pages: updatedPages });
}
get description(): string {
const direction = this.rotation > 0 ? 'right' : 'left';
return `Rotate ${this.pageIds.length} page(s) ${direction}`;
}
}
// Delete pages command
export class DeletePagesCommand extends PageCommand {
private pageIds: string[];
private deletedPages: PDFPage[];
private deletedPositions: Map<string, number>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[]
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.deletedPages = [];
this.deletedPositions = new Map();
}
execute(): void {
// Store deleted pages and their positions for undo
this.deletedPages = this.pdfDocument.pages.filter(page =>
this.pageIds.includes(page.id)
);
this.deletedPages.forEach(page => {
const index = this.pdfDocument.pages.findIndex(p => p.id === page.id);
this.deletedPositions.set(page.id, index);
});
const updatedPages = this.pdfDocument.pages
.filter(page => !this.pageIds.includes(page.id))
.map((page, index) => ({ ...page, pageNumber: index + 1 }));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
undo(): void {
// Simply restore to the previous state (before deletion)
this.setPdfDocument(this.previousState);
}
get description(): string {
return `Delete ${this.pageIds.length} page(s)`;
}
}
// Move pages command
export class MovePagesCommand extends PageCommand {
private pageIds: string[];
private targetIndex: number;
private originalIndices: Map<string, number>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[],
targetIndex: number
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.targetIndex = targetIndex;
this.originalIndices = new Map();
}
execute(): void {
// Store original positions
this.pageIds.forEach(pageId => {
const index = this.pdfDocument.pages.findIndex(p => p.id === pageId);
this.originalIndices.set(pageId, index);
});
let newPages = [...this.pdfDocument.pages];
const pagesToMove = this.pageIds
.map(id => this.pdfDocument.pages.find(p => p.id === id))
.filter((page): page is PDFPage => page !== undefined);
// Remove pages to move
newPages = newPages.filter(page => !this.pageIds.includes(page.id));
// Insert pages at target position
newPages.splice(this.targetIndex, 0, ...pagesToMove);
// Update page numbers
newPages = newPages.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({ ...this.pdfDocument, pages: newPages });
}
get description(): string {
return `Move ${this.pageIds.length} page(s)`;
}
}
// Reorder single page command (for drag-and-drop)
export class ReorderPageCommand extends PageCommand {
private pageId: string;
private targetIndex: number;
private originalIndex: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageId: string,
targetIndex: number
) {
super(pdfDocument, setPdfDocument);
this.pageId = pageId;
this.targetIndex = targetIndex;
this.originalIndex = pdfDocument.pages.findIndex(p => p.id === pageId);
}
execute(): void {
const newPages = [...this.pdfDocument.pages];
const [movedPage] = newPages.splice(this.originalIndex, 1);
newPages.splice(this.targetIndex, 0, movedPage);
// Update page numbers
const updatedPages = newPages.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({ ...this.pdfDocument, pages: updatedPages });
}
get description(): string {
return `Reorder page ${this.originalIndex + 1} to position ${this.targetIndex + 1}`;
}
}
// Toggle split markers command
export class ToggleSplitCommand extends PageCommand {
private pageIds: string[];
private previousSplitStates: Map<string, boolean>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[]
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.previousSplitStates = new Map();
}
execute(): void {
// Store previous split states
this.pageIds.forEach(pageId => {
const page = this.pdfDocument.pages.find(p => p.id === pageId);
if (page) {
this.previousSplitStates.set(pageId, !!page.splitBefore);
}
});
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
return { ...page, splitBefore: !page.splitBefore };
}
return page;
});
this.setPdfDocument({ ...this.pdfDocument, pages: updatedPages });
}
undo(): void {
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
const previousState = this.previousSplitStates.get(page.id);
return { ...page, splitBefore: previousState };
}
return page;
});
this.setPdfDocument({ ...this.pdfDocument, pages: updatedPages });
}
get description(): string {
return `Toggle split markers for ${this.pageIds.length} page(s)`;
}
}
// Add pages command (for inserting new files)
export class AddPagesCommand extends PageCommand {
private newPages: PDFPage[];
private insertIndex: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
newPages: PDFPage[],
insertIndex: number = -1 // -1 means append to end
) {
super(pdfDocument, setPdfDocument);
this.newPages = newPages;
this.insertIndex = insertIndex === -1 ? pdfDocument.pages.length : insertIndex;
}
execute(): void {
const newPagesArray = [...this.pdfDocument.pages];
newPagesArray.splice(this.insertIndex, 0, ...this.newPages);
// Update page numbers for all pages
const updatedPages = newPagesArray.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
undo(): void {
const updatedPages = this.pdfDocument.pages
.filter(page => !this.newPages.some(newPage => newPage.id === page.id))
.map((page, index) => ({ ...page, pageNumber: index + 1 }));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
get description(): string {
return `Add ${this.newPages.length} page(s)`;
}
}
// Command sequence for bulk operations
export class PageCommandSequence implements CommandSequence {
commands: Command[];
description: string;
constructor(commands: Command[], description?: string) {
this.commands = commands;
this.description = description || `Execute ${commands.length} operations`;
}
execute(): void {
this.commands.forEach(command => command.execute());
}
undo(): void {
// Undo in reverse order
[...this.commands].reverse().forEach(command => command.undo());
}
}
@@ -1,136 +0,0 @@
import React from "react";
import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon } from "@mantine/core";
import { useTranslation } from "react-i18next";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import StorageIcon from "@mui/icons-material/Storage";
import { FileWithUrl } from "../types/file";
import { getFileSize, getFileDate } from "../utils/fileUtils";
import { useIndexedDBThumbnail } from "../hooks/useIndexedDBThumbnail";
interface FileCardProps {
file: FileWithUrl;
onRemove: () => void;
onDoubleClick?: () => void;
}
const FileCard: React.FC<FileCardProps> = ({ file, onRemove, onDoubleClick }) => {
const { t } = useTranslation();
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
return (
<Card
shadow="xs"
radius="md"
withBorder
p="xs"
style={{
width: 225,
minWidth: 180,
maxWidth: 260,
cursor: onDoubleClick ? "pointer" : undefined
}}
onDoubleClick={onDoubleClick}
>
<Stack gap={6} align="center">
<Box
style={{
border: "2px solid #e0e0e0",
borderRadius: 8,
width: 90,
height: 120,
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "0 auto",
background: "#fafbfc",
}}
>
{thumb ? (
<Image
src={thumb}
alt="PDF thumbnail"
height={110}
width={80}
fit="contain"
radius="sm"
/>
) : isGenerating ? (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
width: 20,
height: 20,
border: '2px solid #ddd',
borderTop: '2px solid #666',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: 8
}} />
<Text size="xs" c="dimmed">Generating...</Text>
</div>
) : (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<ThemeIcon
variant="light"
color={file.size > 100 * 1024 * 1024 ? "orange" : "red"}
size={60}
radius="sm"
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
>
<PictureAsPdfIcon style={{ fontSize: 40 }} />
</ThemeIcon>
{file.size > 100 * 1024 * 1024 && (
<Text size="xs" c="dimmed" mt={4}>Large File</Text>
)}
</div>
)}
</Box>
<Text fw={500} size="sm" lineClamp={1} ta="center">
{file.name}
</Text>
<Group gap="xs" justify="center">
<Badge color="gray" variant="light" size="sm">
{getFileSize(file)}
</Badge>
<Badge color="blue" variant="light" size="sm">
{getFileDate(file)}
</Badge>
{file.storedInIndexedDB && (
<Badge
color="green"
variant="light"
size="sm"
leftSection={<StorageIcon style={{ fontSize: 12 }} />}
>
DB
</Badge>
)}
</Group>
<Button
color="red"
size="xs"
variant="light"
onClick={onRemove}
mt={4}
>
{t("delete", "Remove")}
</Button>
</Stack>
</Card>
);
};
export default FileCard;
+19 -26
View File
@@ -1,7 +1,7 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Modal } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { FileWithUrl } from '../types/file';
import { StirlingFileStub } from '../types/fileContext';
import { useFileManager } from '../hooks/useFileManager';
import { useFilesModalContext } from '../contexts/FilesModalContext';
import { Tool } from '../types/tool';
@@ -15,12 +15,12 @@ interface FileManagerProps {
}
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const { isFilesModalOpen, closeFilesModal, onFilesSelect } = useFilesModalContext();
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
const { isFilesModalOpen, closeFilesModal, onFileUpload, onRecentFileSelect } = useFilesModalContext();
const [recentFiles, setRecentFiles] = useState<StirlingFileStub[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
const { loadRecentFiles, handleRemoveFile } = useFileManager();
// File management handlers
const isFileSupported = useCallback((fileName: string) => {
@@ -34,30 +34,26 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
setRecentFiles(files);
}, [loadRecentFiles]);
const handleFilesSelected = useCallback(async (files: FileWithUrl[]) => {
const handleRecentFilesSelected = useCallback(async (files: StirlingFileStub[]) => {
try {
const fileObjects = await Promise.all(
files.map(async (fileWithUrl) => {
return await convertToFile(fileWithUrl);
})
);
onFilesSelect(fileObjects);
// Use StirlingFileStubs directly - preserves all metadata!
onRecentFileSelect(files);
} catch (error) {
console.error('Failed to process selected files:', error);
}
}, [convertToFile, onFilesSelect]);
}, [onRecentFileSelect]);
const handleNewFileUpload = useCallback(async (files: File[]) => {
if (files.length > 0) {
try {
// Files will get IDs assigned through onFilesSelect -> FileContext addFiles
onFilesSelect(files);
onFileUpload(files);
await refreshRecentFiles();
} catch (error) {
console.error('Failed to process dropped files:', error);
}
}
}, [onFilesSelect, refreshRecentFiles]);
}, [onFileUpload, refreshRecentFiles]);
const handleRemoveFileByIndex = useCallback(async (index: number) => {
await handleRemoveFile(index, recentFiles, setRecentFiles);
@@ -82,14 +78,11 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
// Cleanup any blob URLs when component unmounts
useEffect(() => {
return () => {
// Clean up blob URLs from recent files
recentFiles.forEach(file => {
if (file.url && file.url.startsWith('blob:')) {
URL.revokeObjectURL(file.url);
}
});
// StoredFileMetadata doesn't have blob URLs, so no cleanup needed
// Blob URLs are managed by FileContext and tool operations
console.log('FileManager unmounting - FileContext handles blob URL cleanup');
};
}, [recentFiles]);
}, []);
// Modal size constants for consistent scaling
const modalHeight = '80vh';
@@ -104,7 +97,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
onClose={closeFilesModal}
size={isMobile ? "100%" : "auto"}
centered
radius={30}
radius="md"
className="overflow-hidden p-0"
withCloseButton={false}
styles={{
@@ -130,14 +123,14 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
onDrop={handleNewFileUpload}
onDragEnter={() => setIsDragging(true)}
onDragLeave={() => setIsDragging(false)}
accept={["*/*"] as any}
accept={{}}
multiple={true}
activateOnClick={false}
style={{
height: '100%',
width: '100%',
border: 'none',
borderRadius: '30px',
borderRadius: 'var(--radius-md)',
backgroundColor: 'var(--bg-file-manager)'
}}
styles={{
@@ -146,13 +139,13 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
>
<FileManagerProvider
recentFiles={recentFiles}
onFilesSelected={handleFilesSelected}
onRecentFilesSelected={handleRecentFilesSelected}
onNewFilesSelect={handleNewFileUpload}
onClose={closeFilesModal}
isFileSupported={isFileSupported}
isOpen={isFilesModalOpen}
onFileRemove={handleRemoveFileByIndex}
modalHeight={modalHeight}
storeFile={storeFile}
refreshRecentFiles={refreshRecentFiles}
>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
@@ -0,0 +1,276 @@
/* =========================
FileEditor Card UI Styles
========================= */
.card {
background: var(--file-card-bg);
border-radius: 0.0625rem;
cursor: pointer;
transition: box-shadow 0.18s ease, outline-color 0.18s ease, transform 0.18s ease;
max-width: 100%;
max-height: 100%;
overflow: hidden;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card[data-selected="true"] {
box-shadow: var(--shadow-sm);
}
/* While dragging */
.card.dragging,
.card:global(.dragging) {
outline: 1px solid var(--border-strong);
box-shadow: var(--shadow-md);
transform: none !important;
}
/* -------- Header -------- */
.header {
height: 2.25rem;
border-radius: 0.0625rem 0.0625rem 0 0;
display: grid;
grid-template-columns: 44px 1fr 44px;
align-items: center;
padding: 0 6px;
user-select: none;
background: var(--bg-toolbar);
color: var(--text-primary);
border-bottom: 1px solid var(--border-default);
}
.headerResting {
background: #3B4B6E; /* dark blue for unselected in light mode */
color: #FFFFFF;
border-bottom: 1px solid var(--border-default);
}
.headerSelected {
background: var(--header-selected-bg);
color: var(--header-selected-fg);
border-bottom: 1px solid var(--header-selected-bg);
}
/* Selected border color in light mode */
:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] {
outline-color: var(--card-selected-border);
}
/* Reserve space for checkbox instead of logo */
.logoMark {
margin-left: 8px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.headerIndex {
text-align: center;
font-weight: 500;
font-size: 18px;
letter-spacing: 0.04em;
}
.kebab {
justify-self: end;
}
/* Menu dropdown */
.menuDropdown {
min-width: 210px;
}
/* -------- Title / Meta -------- */
.title {
line-height: 1.2;
color: var(--text-primary);
}
.meta {
margin-top: 2px;
color: var(--text-secondary);
}
/* -------- Preview area -------- */
.previewBox {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--file-card-bg);
}
.previewPaper {
width: 100%;
height: calc(100% - 6px);
min-height: 9rem;
justify-content: center;
display: grid;
position: relative;
overflow: hidden;
background: var(--file-card-bg);
}
/* Thumbnail fallback */
.previewPaper[data-thumb-missing="true"]::after {
content: "No preview";
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--text-secondary);
font-weight: 600;
font-size: 12px;
}
/* Drag handle grip */
.dragHandle {
position: absolute;
bottom: 6px;
right: 6px;
color: var(--text-secondary);
z-index: 1;
cursor: grab;
display: inline-flex;
}
/* Actions Overlay */
.actionsOverlay {
position: absolute;
left: 0;
top: 44px; /* just below header */
background: var(--bg-toolbar);
border-bottom: 1px solid var(--border-default);
z-index: 20;
overflow: hidden;
animation: slideDown 140ms ease-out;
color: var(--text-primary);
}
@keyframes slideDown {
from {
transform: translateY(-8px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.actionRow {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
text-align: left;
}
.actionRow:hover {
background: var(--hover-bg);
}
.actionDanger {
color: var(--text-brand-accent);
}
.actionsDivider {
height: 1px;
background: var(--border-default);
margin: 4px 0;
}
/* Pin indicator */
.pinIndicator {
position: absolute;
bottom: 4px;
left: 4px;
z-index: 1;
color: rgba(0, 0, 0, 0.35);
}
/* Unsupported file indicator */
.unsupportedPill {
margin-left: 1.75rem;
background: #6B7280;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
height: 20px;
}
/* Animations */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.pulse {
animation: pulse 1s infinite;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.card,
.menuDropdown {
transition: none !important;
}
}
/* =========================
DARK MODE OVERRIDES
========================= */
:global([data-mantine-color-scheme="dark"]) .card {
outline-color: #3A4047; /* deselected stroke */
}
:global([data-mantine-color-scheme="dark"]) .card[data-selected="true"] {
outline-color: #4B525A; /* selected stroke (subtle grey) */
}
:global([data-mantine-color-scheme="dark"]) .headerResting {
background: #1F2329; /* requested default unselected color */
color: var(--tool-header-text); /* #D0D6DC */
border-bottom-color: var(--tool-header-border); /* #3A4047 */
}
:global([data-mantine-color-scheme="dark"]) .headerSelected {
background: var(--tool-header-border); /* #3A4047 */
color: var(--tool-header-text); /* #D0D6DC */
border-bottom-color: var(--tool-header-border);
}
:global([data-mantine-color-scheme="dark"]) .title {
color: #D0D6DC; /* title text */
}
:global([data-mantine-color-scheme="dark"]) .meta {
color: #6B7280; /* subtitle text */
}
/* Light mode selected header stroke override */
:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] {
outline-color: #3B4B6E;
}
+163 -568
View File
@@ -1,52 +1,28 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import React, { useState, useCallback, useRef, useMemo } from 'react';
import {
Button, Text, Center, Box, Notification, TextInput, LoadingOverlay, Modal, Alert, Container,
Stack, Group
Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useTranslation } from 'react-i18next';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useFileContext } from '../../contexts/FileContext';
import { useFileSelection } from '../../contexts/FileSelectionContext';
import { FileOperation } from '../../types/fileContext';
import { fileStorage } from '../../services/fileStorage';
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext';
import { useNavigationActions } from '../../contexts/NavigationContext';
import { zipFileService } from '../../services/zipFileService';
import { detectFileExtension } from '../../utils/fileUtils';
import styles from '../pageEditor/PageEditor.module.css';
import FileThumbnail from '../pageEditor/FileThumbnail';
import DragDropGrid from '../pageEditor/DragDropGrid';
import FileEditorThumbnail from './FileEditorThumbnail';
import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader';
interface FileItem {
id: string;
name: string;
pageCount: number;
thumbnail: string;
size: number;
file: File;
splitBefore?: boolean;
}
import { FileId, StirlingFile } from '../../types/fileContext';
interface FileEditorProps {
onOpenPageEditor?: (file: File) => void;
onMergeFiles?: (files: File[]) => void;
onOpenPageEditor?: () => void;
onMergeFiles?: (files: StirlingFile[]) => void;
toolMode?: boolean;
showUpload?: boolean;
showBulkActions?: boolean;
supportedExtensions?: string[];
}
const FileEditor = ({
onOpenPageEditor,
onMergeFiles,
toolMode = false,
showUpload = true,
showBulkActions = true,
supportedExtensions = ["pdf"]
}: FileEditorProps) => {
const { t } = useTranslation();
// Utility function to check if a file extension is supported
const isFileSupported = useCallback((fileName: string): boolean => {
@@ -54,33 +30,22 @@ const FileEditor = ({
return extension ? supportedExtensions.includes(extension) : false;
}, [supportedExtensions]);
// Get file context
const fileContext = useFileContext();
const {
activeFiles,
processedFiles,
selectedFileIds,
setSelectedFiles: setContextSelectedFiles,
isProcessing,
addFiles,
removeFiles,
setCurrentView,
recordOperation,
markOperationApplied
} = fileContext;
// Use optimized FileContext hooks
const { state, selectors } = useFileState();
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
// Extract needed values from state (memoized to prevent infinite loops)
const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [selectors.getFilesSignature()]);
const selectedFileIds = state.ui.selectedFileIds;
// Get navigation actions
const { actions: navActions } = useNavigationActions();
// Get file selection context
const {
selectedFiles: toolSelectedFiles,
setSelectedFiles: setToolSelectedFiles,
maxFiles,
isToolMode
} = useFileSelection();
const { setSelectedFiles } = useFileSelection();
const [files, setFiles] = useState<FileItem[]>([]);
const [status, setStatus] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [localLoading, setLocalLoading] = useState(false);
const [selectionMode, setSelectionMode] = useState(toolMode);
// Enable selection mode automatically in tool mode
@@ -89,13 +54,7 @@ const FileEditor = ({
setSelectionMode(true);
}
}, [toolMode]);
const [draggedFile, setDraggedFile] = useState<string | null>(null);
const [dropTarget, setDropTarget] = useState<string | null>(null);
const [multiFileDrag, setMultiFileDrag] = useState<{fileIds: string[], count: number} | null>(null);
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
const [showFilePickerModal, setShowFilePickerModal] = useState(false);
const [conversionProgress, setConversionProgress] = useState(0);
const [zipExtractionProgress, setZipExtractionProgress] = useState<{
isExtracting: boolean;
currentFile: string;
@@ -109,116 +68,15 @@ const FileEditor = ({
extractedCount: 0,
totalFiles: 0
});
const fileRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const lastActiveFilesRef = useRef<string[]>([]);
const lastProcessedFilesRef = useRef<number>(0);
// Get selected file IDs from context (defensive programming)
const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
// Map context selections to local file IDs for UI display
const localSelectedIds = files
.filter(file => {
const fileId = (file.file as any).id || file.name;
return contextSelectedIds.includes(fileId);
})
.map(file => file.id);
// Convert shared files to FileEditor format
const convertToFileItem = useCallback(async (sharedFile: any): Promise<FileItem> => {
// Generate thumbnail if not already available
const thumbnail = sharedFile.thumbnail || await generateThumbnailForFile(sharedFile.file || sharedFile);
return {
id: sharedFile.id || `file-${Date.now()}-${Math.random()}`,
name: (sharedFile.file?.name || sharedFile.name || 'unknown'),
pageCount: sharedFile.pageCount || Math.floor(Math.random() * 20) + 1, // Mock for now
thumbnail,
size: sharedFile.file?.size || sharedFile.size || 0,
file: sharedFile.file || sharedFile,
};
}, []);
// Convert activeFiles to FileItem format using context (async to avoid blocking)
useEffect(() => {
// Check if the actual content has changed, not just references
const currentActiveFileNames = activeFiles.map(f => f.name);
const currentProcessedFilesSize = processedFiles.size;
const activeFilesChanged = JSON.stringify(currentActiveFileNames) !== JSON.stringify(lastActiveFilesRef.current);
const processedFilesChanged = currentProcessedFilesSize !== lastProcessedFilesRef.current;
if (!activeFilesChanged && !processedFilesChanged) {
return;
}
// Update refs
lastActiveFilesRef.current = currentActiveFileNames;
lastProcessedFilesRef.current = currentProcessedFilesSize;
const convertActiveFiles = async () => {
if (activeFiles.length > 0) {
setLocalLoading(true);
try {
// Process files in chunks to avoid blocking UI
const convertedFiles: FileItem[] = [];
for (let i = 0; i < activeFiles.length; i++) {
const file = activeFiles[i];
// Try to get thumbnail from processed file first
const processedFile = processedFiles.get(file);
let thumbnail = processedFile?.pages?.[0]?.thumbnail;
// If no thumbnail from processed file, try to generate one
if (!thumbnail) {
try {
thumbnail = await generateThumbnailForFile(file);
} catch (error) {
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
thumbnail = undefined; // Use placeholder
}
}
const convertedFile = {
id: `file-${Date.now()}-${Math.random()}`,
name: file.name,
pageCount: processedFile?.totalPages || Math.floor(Math.random() * 20) + 1,
thumbnail: thumbnail || '',
size: file.size,
file,
};
convertedFiles.push(convertedFile);
// Update progress
setConversionProgress(((i + 1) / activeFiles.length) * 100);
// Yield to main thread between files
if (i < activeFiles.length - 1) {
await new Promise(resolve => requestAnimationFrame(resolve));
}
}
setFiles(convertedFiles);
} catch (err) {
console.error('Error converting active files:', err);
} finally {
setLocalLoading(false);
setConversionProgress(0);
}
} else {
setFiles([]);
setLocalLoading(false);
setConversionProgress(0);
}
};
convertActiveFiles();
}, [activeFiles, processedFiles]);
// Create refs for frequently changing values to stabilize callbacks
const contextSelectedIdsRef = useRef<FileId[]>([]);
contextSelectedIdsRef.current = contextSelectedIds;
// Use activeStirlingFileStubs directly - no conversion needed
const localSelectedIds = contextSelectedIds;
// Process uploaded files using context
const handleFileUpload = useCallback(async (uploadedFiles: File[]) => {
@@ -270,38 +128,14 @@ const FileEditor = ({
if (extractionResult.success) {
allExtractedFiles.push(...extractionResult.extractedFiles);
// Record ZIP extraction operation
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const operation: FileOperation = {
id: operationId,
type: 'convert',
timestamp: Date.now(),
fileIds: extractionResult.extractedFiles.map(f => f.name),
status: 'pending',
metadata: {
originalFileName: file.name,
outputFileNames: extractionResult.extractedFiles.map(f => f.name),
fileSize: file.size,
parameters: {
extractionType: 'zip',
extractedCount: extractionResult.extractedCount,
totalFiles: extractionResult.totalFiles
}
if (extractionResult.errors.length > 0) {
errors.push(...extractionResult.errors);
}
};
recordOperation(file.name, operation);
markOperationApplied(file.name, operationId);
if (extractionResult.errors.length > 0) {
errors.push(...extractionResult.errors);
}
} else {
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
}
} else {
// ZIP doesn't contain PDFs or is invalid - treat as regular file
console.log(`Adding ZIP file as regular file: ${file.name} (no PDFs found)`);
allExtractedFiles.push(file);
}
} catch (zipError) {
@@ -315,7 +149,6 @@ const FileEditor = ({
});
}
} else {
console.log(`Adding none PDF file: ${file.name} (${file.type})`);
allExtractedFiles.push(file);
}
}
@@ -327,28 +160,6 @@ const FileEditor = ({
// Process all extracted files
if (allExtractedFiles.length > 0) {
// Record upload operations for PDF files
for (const file of allExtractedFiles) {
const operationId = `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const operation: FileOperation = {
id: operationId,
type: 'upload',
timestamp: Date.now(),
fileIds: [file.name],
status: 'pending',
metadata: {
originalFileName: file.name,
fileSize: file.size,
parameters: {
uploadMethod: 'drag-drop'
}
}
};
recordOperation(file.name, operation);
markOperationApplied(file.name, operationId);
}
// Add files to context (they will be processed automatically)
await addFiles(allExtractedFiles);
setStatus(`Added ${allExtractedFiles.length} files`);
@@ -367,298 +178,139 @@ const FileEditor = ({
totalFiles: 0
});
}
}, [addFiles, recordOperation, markOperationApplied]);
}, [addFiles]);
const selectAll = useCallback(() => {
setContextSelectedFiles(files.map(f => (f.file as any).id || f.name));
}, [files, setContextSelectedFiles]);
const toggleFile = useCallback((fileId: FileId) => {
const currentSelectedIds = contextSelectedIdsRef.current;
const deselectAll = useCallback(() => setContextSelectedFiles([]), [setContextSelectedFiles]);
const targetRecord = activeStirlingFileStubs.find(r => r.id === fileId);
if (!targetRecord) return;
const closeAllFiles = useCallback(() => {
if (activeFiles.length === 0) return;
const contextFileId = fileId; // No need to create a new ID
const isSelected = currentSelectedIds.includes(contextFileId);
// Record close all operation for each file
activeFiles.forEach(file => {
const operationId = `close-all-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const operation: FileOperation = {
id: operationId,
type: 'remove',
timestamp: Date.now(),
fileIds: [file.name],
status: 'pending',
metadata: {
originalFileName: file.name,
fileSize: file.size,
parameters: {
action: 'close_all',
reason: 'user_request'
}
}
};
recordOperation(file.name, operation);
markOperationApplied(file.name, operationId);
});
// Remove all files from context but keep in storage
removeFiles(activeFiles.map(f => (f as any).id || f.name), false);
// Clear selections
setContextSelectedFiles([]);
}, [activeFiles, removeFiles, setContextSelectedFiles, recordOperation, markOperationApplied]);
const toggleFile = useCallback((fileId: string) => {
const targetFile = files.find(f => f.id === fileId);
if (!targetFile) return;
const contextFileId = (targetFile.file as any).id || targetFile.name;
const isSelected = contextSelectedIds.includes(contextFileId);
let newSelection: string[];
let newSelection: FileId[];
if (isSelected) {
// Remove file from selection
newSelection = contextSelectedIds.filter(id => id !== contextFileId);
newSelection = currentSelectedIds.filter(id => id !== contextFileId);
} else {
// Add file to selection
if (maxFiles === 1) {
// In tool mode, typically allow multiple files unless specified otherwise
const maxAllowed = toolMode ? 10 : Infinity; // Default max for tools
if (maxAllowed === 1) {
newSelection = [contextFileId];
} else {
// Check if we've hit the selection limit
if (maxFiles > 1 && contextSelectedIds.length >= maxFiles) {
setStatus(`Maximum ${maxFiles} files can be selected`);
if (maxAllowed > 1 && currentSelectedIds.length >= maxAllowed) {
setStatus(`Maximum ${maxAllowed} files can be selected`);
return;
}
newSelection = [...contextSelectedIds, contextFileId];
newSelection = [...currentSelectedIds, contextFileId];
}
}
// Update context
setContextSelectedFiles(newSelection);
// Update tool selection context if in tool mode
if (isToolMode || toolMode) {
const selectedFiles = files
.filter(f => {
const fId = (f.file as any).id || f.name;
return newSelection.includes(fId);
})
.map(f => f.file);
setToolSelectedFiles(selectedFiles);
}
}, [files, setContextSelectedFiles, maxFiles, contextSelectedIds, setStatus, isToolMode, toolMode, setToolSelectedFiles]);
const toggleSelectionMode = useCallback(() => {
setSelectionMode(prev => {
const newMode = !prev;
if (!newMode) {
setContextSelectedFiles([]);
}
return newMode;
});
}, [setContextSelectedFiles]);
// Update context (this automatically updates tool selection since they use the same action)
setSelectedFiles(newSelection);
}, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]);
// Drag and drop handlers
const handleDragStart = useCallback((fileId: string) => {
setDraggedFile(fileId);
// File reordering handler for drag and drop
const handleReorderFiles = useCallback((sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => {
const currentIds = activeStirlingFileStubs.map(r => r.id);
if (selectionMode && localSelectedIds.includes(fileId) && localSelectedIds.length > 1) {
setMultiFileDrag({
fileIds: localSelectedIds,
count: localSelectedIds.length
});
} else {
setMultiFileDrag(null);
}
}, [selectionMode, localSelectedIds]);
// Find indices
const sourceIndex = currentIds.findIndex(id => id === sourceFileId);
const targetIndex = currentIds.findIndex(id => id === targetFileId);
const handleDragEnd = useCallback(() => {
setDraggedFile(null);
setDropTarget(null);
setMultiFileDrag(null);
setDragPosition(null);
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
if (!draggedFile) return;
if (multiFileDrag) {
setDragPosition({ x: e.clientX, y: e.clientY });
}
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
if (!elementUnderCursor) return;
const fileContainer = elementUnderCursor.closest('[data-file-id]');
if (fileContainer) {
const fileId = fileContainer.getAttribute('data-file-id');
if (fileId && fileId !== draggedFile) {
setDropTarget(fileId);
return;
}
}
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
if (endZone) {
setDropTarget('end');
if (sourceIndex === -1 || targetIndex === -1) {
console.warn('Could not find source or target file for reordering');
return;
}
setDropTarget(null);
}, [draggedFile, multiFileDrag]);
// Handle multi-file selection reordering
const filesToMove = selectedFileIds.length > 1
? selectedFileIds.filter(id => currentIds.includes(id))
: [sourceFileId];
const handleDragEnter = useCallback((fileId: string) => {
if (draggedFile && fileId !== draggedFile) {
setDropTarget(fileId);
}
}, [draggedFile]);
// Create new order
const newOrder = [...currentIds];
const handleDragLeave = useCallback(() => {
// Let dragover handle this
}, []);
// Remove files to move from their current positions (in reverse order to maintain indices)
const sourceIndices = filesToMove.map(id => newOrder.findIndex(nId => nId === id))
.sort((a, b) => b - a); // Sort descending
const handleDrop = useCallback((e: React.DragEvent, targetFileId: string | 'end') => {
e.preventDefault();
if (!draggedFile || draggedFile === targetFileId) return;
let targetIndex: number;
if (targetFileId === 'end') {
targetIndex = files.length;
} else {
targetIndex = files.findIndex(f => f.id === targetFileId);
if (targetIndex === -1) return;
}
const filesToMove = selectionMode && localSelectedIds.includes(draggedFile)
? localSelectedIds
: [draggedFile];
// Update the local files state and sync with activeFiles
setFiles(prev => {
const newFiles = [...prev];
const movedFiles = filesToMove.map(id => newFiles.find(f => f.id === id)!).filter(Boolean);
// Remove moved files
filesToMove.forEach(id => {
const index = newFiles.findIndex(f => f.id === id);
if (index !== -1) newFiles.splice(index, 1);
});
// Insert at target position
newFiles.splice(targetIndex, 0, ...movedFiles);
// TODO: Update context with reordered files (need to implement file reordering in context)
// For now, just return the reordered local state
return newFiles;
sourceIndices.forEach(index => {
newOrder.splice(index, 1);
});
const moveCount = multiFileDrag ? multiFileDrag.count : 1;
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
}, [draggedFile, files, selectionMode, localSelectedIds, multiFileDrag]);
const handleEndZoneDragEnter = useCallback(() => {
if (draggedFile) {
setDropTarget('end');
// Calculate insertion index after removals
let insertIndex = newOrder.findIndex(id => id === targetFileId);
if (insertIndex !== -1) {
// Determine if moving forward or backward
const isMovingForward = sourceIndex < targetIndex;
if (isMovingForward) {
// Moving forward: insert after target
insertIndex += 1;
} else {
// Moving backward: insert before target (insertIndex already correct)
}
} else {
// Target was moved, insert at end
insertIndex = newOrder.length;
}
}, [draggedFile]);
// Insert files at the calculated position
newOrder.splice(insertIndex, 0, ...filesToMove);
// Update file order
reorderFiles(newOrder);
// Update status
const moveCount = filesToMove.length;
setStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
}, [activeStirlingFileStubs, reorderFiles, setStatus]);
// File operations using context
const handleDeleteFile = useCallback((fileId: string) => {
console.log('handleDeleteFile called with fileId:', fileId);
const file = files.find(f => f.id === fileId);
console.log('Found file:', file);
if (file) {
console.log('Attempting to remove file:', file.name);
console.log('Actual file object:', file.file);
console.log('Actual file.file.name:', file.file.name);
// Record close operation
const fileName = file.file.name;
const fileId = (file.file as any).id || fileName;
const operationId = `close-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const operation: FileOperation = {
id: operationId,
type: 'remove',
timestamp: Date.now(),
fileIds: [fileName],
status: 'pending',
metadata: {
originalFileName: fileName,
fileSize: file.size,
parameters: {
action: 'close',
reason: 'user_request'
}
}
};
recordOperation(fileName, operation);
const handleDeleteFile = useCallback((fileId: FileId) => {
const record = activeStirlingFileStubs.find(r => r.id === fileId);
const file = record ? selectors.getFile(record.id) : null;
if (record && file) {
// Remove file from context but keep in storage (close, don't delete)
console.log('Calling removeFiles with:', [fileId]);
removeFiles([fileId], false);
const contextFileId = record.id;
removeFiles([contextFileId], false);
// Remove from context selections
const newSelection = contextSelectedIds.filter(id => id !== fileId);
setContextSelectedFiles(newSelection);
// Mark operation as applied
markOperationApplied(fileName, operationId);
} else {
console.log('File not found for fileId:', fileId);
const currentSelected = selectedFileIds.filter(id => id !== contextFileId);
setSelectedFiles(currentSelected);
}
}, [files, removeFiles, setContextSelectedFiles, recordOperation, markOperationApplied]);
}, [activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds]);
const handleViewFile = useCallback((fileId: string) => {
const file = files.find(f => f.id === fileId);
if (file) {
// Set the file as selected in context and switch to page editor view
const contextFileId = (file.file as any).id || file.name;
setContextSelectedFiles([contextFileId]);
setCurrentView('pageEditor');
onOpenPageEditor?.(file.file);
const handleViewFile = useCallback((fileId: FileId) => {
const record = activeStirlingFileStubs.find(r => r.id === fileId);
if (record) {
// Set the file as selected in context and switch to viewer for preview
setSelectedFiles([fileId]);
navActions.setWorkbench('viewer');
}
}, [files, setContextSelectedFiles, setCurrentView, onOpenPageEditor]);
}, [activeStirlingFileStubs, setSelectedFiles, navActions.setWorkbench]);
const handleMergeFromHere = useCallback((fileId: string) => {
const startIndex = files.findIndex(f => f.id === fileId);
if (startIndex === -1) return;
const filesToMerge = files.slice(startIndex).map(f => f.file);
if (onMergeFiles) {
onMergeFiles(filesToMerge);
}
}, [files, onMergeFiles]);
const handleSplitFile = useCallback((fileId: string) => {
const file = files.find(f => f.id === fileId);
if (file && onOpenPageEditor) {
onOpenPageEditor(file.file);
}
}, [files, onOpenPageEditor]);
const handleLoadFromStorage = useCallback(async (selectedFiles: any[]) => {
const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
if (selectedFiles.length === 0) return;
setLocalLoading(true);
try {
const convertedFiles = await Promise.all(
selectedFiles.map(convertToFileItem)
);
setFiles(prev => [...prev, ...convertedFiles]);
// Use FileContext to handle loading stored files
// The files are already in FileContext, just need to add them to active files
setStatus(`Loaded ${selectedFiles.length} files from storage`);
} catch (err) {
console.error('Error loading files from storage:', err);
setError('Failed to load some files from storage');
} finally {
setLocalLoading(false);
}
}, [convertToFileItem]);
}, []);
return (
@@ -667,7 +319,6 @@ const FileEditor = ({
multiple={true}
maxSize={2 * 1024 * 1024 * 1024}
style={{
height: '100vh',
border: 'none',
borderRadius: 0,
backgroundColor: 'transparent'
@@ -675,24 +326,13 @@ const FileEditor = ({
activateOnClick={false}
activateOnDrag={true}
>
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
<Box pos="relative" style={{ overflow: 'auto' }}>
<LoadingOverlay visible={false} />
<Box p="md" pt="xl">
<Group mb="md">
{showBulkActions && !toolMode && (
<>
<Button onClick={selectAll} variant="light">Select All</Button>
<Button onClick={deselectAll} variant="light">Deselect All</Button>
<Button onClick={closeAllFiles} variant="light" color="orange">
Close All
</Button>
</>
)}
</Group>
{files.length === 0 && !localLoading && !zipExtractionProgress.isExtracting ? (
{activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
<Center h="60vh">
<Stack align="center" gap="md">
<Text size="lg" c="dimmed">📁</Text>
@@ -700,7 +340,7 @@ const FileEditor = ({
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
</Stack>
</Center>
) : files.length === 0 && (localLoading || zipExtractionProgress.isExtracting) ? (
) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
<Box>
<SkeletonLoader type="controls" />
@@ -734,88 +374,39 @@ const FileEditor = ({
</Box>
)}
{/* Processing indicator */}
{localLoading && (
<Box mb="md" p="sm" style={{ backgroundColor: 'var(--mantine-color-blue-0)', borderRadius: 8 }}>
<Group justify="space-between" mb="xs">
<Text size="sm" fw={500}>Loading files...</Text>
<Text size="sm" c="dimmed">{Math.round(conversionProgress)}%</Text>
</Group>
<div style={{
width: '100%',
height: '4px',
backgroundColor: 'var(--mantine-color-gray-2)',
borderRadius: '2px',
overflow: 'hidden'
}}>
<div style={{
width: `${Math.round(conversionProgress)}%`,
height: '100%',
backgroundColor: 'var(--mantine-color-blue-6)',
transition: 'width 0.3s ease'
}} />
</div>
</Box>
)}
<SkeletonLoader type="fileGrid" count={6} />
</Box>
) : (
<DragDropGrid
items={files}
selectedItems={localSelectedIds as any /* FIX ME */}
selectionMode={selectionMode}
isAnimating={isAnimating}
onDragStart={handleDragStart as any /* FIX ME */}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter as any /* FIX ME */}
onDragLeave={handleDragLeave}
onDrop={handleDrop as any /* FIX ME */}
onEndZoneDragEnter={handleEndZoneDragEnter}
draggedItem={draggedFile as any /* FIX ME */}
dropTarget={dropTarget as any /* FIX ME */}
multiItemDrag={multiFileDrag as any /* FIX ME */}
dragPosition={dragPosition}
renderItem={(file, index, refs) => (
<FileThumbnail
file={file}
index={index}
totalFiles={files.length}
selectedFiles={localSelectedIds}
selectionMode={selectionMode}
draggedFile={draggedFile}
dropTarget={dropTarget}
isAnimating={isAnimating}
fileRefs={refs}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onToggleFile={toggleFile}
onDeleteFile={handleDeleteFile}
onViewFile={handleViewFile}
onSetStatus={setStatus}
toolMode={toolMode}
isSupported={isFileSupported(file.name)}
/>
)}
renderSplitMarker={(file, index) => (
<div
style={{
width: '2px',
height: '24rem',
borderLeft: '2px dashed #3b82f6',
backgroundColor: 'transparent',
marginLeft: '-0.75rem',
marginRight: '-0.75rem',
flexShrink: 0
}}
/>
)}
/>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
rowGap: '1.5rem',
padding: '1rem',
pointerEvents: 'auto'
}}
>
{activeStirlingFileStubs.map((record, index) => {
return (
<FileEditorThumbnail
key={record.id}
file={record}
index={index}
totalFiles={activeStirlingFileStubs.length}
selectedFiles={localSelectedIds}
selectionMode={selectionMode}
onToggleFile={toggleFile}
onDeleteFile={handleDeleteFile}
onViewFile={handleViewFile}
onSetStatus={setStatus}
onReorderFiles={handleReorderFiles}
toolMode={toolMode}
isSupported={isFileSupported(record.name)}
/>
);
})}
</div>
)}
</Box>
@@ -828,25 +419,29 @@ const FileEditor = ({
/>
{status && (
<Notification
color="blue"
mt="md"
onClose={() => setStatus(null)}
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }}
>
{status}
</Notification>
<Portal>
<Notification
color="blue"
mt="md"
onClose={() => setStatus(null)}
style={{ position: 'fixed', bottom: 40, right: 80, zIndex: 10001 }}
>
{status}
</Notification>
</Portal>
)}
{error && (
<Notification
color="red"
mt="md"
onClose={() => setError(null)}
style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 1000 }}
>
{error}
</Notification>
<Portal>
<Notification
color="red"
mt="md"
onClose={() => setError(null)}
style={{ position: 'fixed', bottom: 80, right: 20, zIndex: 10001 }}
>
{error}
</Notification>
</Portal>
)}
</Box>
</Dropzone>
@@ -0,0 +1,427 @@
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core';
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 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 styles from './FileEditor.module.css';
import { useFileContext } from '../../contexts/FileContext';
import { FileId } from '../../types/file';
import ToolChain from '../shared/ToolChain';
interface FileEditorThumbnailProps {
file: StirlingFileStub;
index: number;
totalFiles: number;
selectedFiles: FileId[];
selectionMode: boolean;
onToggleFile: (fileId: FileId) => void;
onDeleteFile: (fileId: FileId) => void;
onViewFile: (fileId: FileId) => void;
onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
onDownloadFile?: (fileId: FileId) => void;
toolMode?: boolean;
isSupported?: boolean;
}
const FileEditorThumbnail = ({
file,
index,
selectedFiles,
onToggleFile,
onDeleteFile,
onSetStatus,
onReorderFiles,
onDownloadFile,
isSupported = true,
}: FileEditorThumbnailProps) => {
const { t } = useTranslation();
const { pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext();
// ---- Drag state ----
const [isDragging, setIsDragging] = useState(false);
const dragElementRef = useRef<HTMLDivElement | null>(null);
const [actionsWidth, setActionsWidth] = useState<number | undefined>(undefined);
const [showActions, setShowActions] = useState(false);
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
return activeFiles.find(f => f.fileId === file.id);
}, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const pageCount = file.processedFile?.totalPages || 0;
const downloadSelectedFile = useCallback(() => {
// Prefer parent-provided handler if available
if (typeof onDownloadFile === 'function') {
onDownloadFile(file.id);
return;
}
// Fallback: attempt to download using the File object if provided
const maybeFile = (file as unknown as { file?: File }).file;
if (maybeFile instanceof File) {
const link = document.createElement('a');
link.href = URL.createObjectURL(maybeFile);
link.download = maybeFile.name || file.name || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
return;
}
// If we can't find a way to download, surface a status message
onSetStatus?.(typeof t === 'function' ? t('downloadUnavailable', 'Download unavailable for this item') : 'Download unavailable for this item');
}, [file, onDownloadFile, onSetStatus, t]);
const handleRef = useRef<HTMLSpanElement | null>(null);
// ---- Selection ----
const isSelected = selectedFiles.includes(file.id);
// ---- Meta formatting ----
const prettySize = useMemo(() => {
const bytes = file.size ?? 0;
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}, [file.size]);
const extUpper = useMemo(() => {
const m = /\.([a-z0-9]+)$/i.exec(file.name ?? '');
return (m?.[1] || '').toUpperCase();
}, [file.name]);
const pageLabel = useMemo(
() =>
pageCount > 0
? `${pageCount} ${pageCount === 1 ? 'Page' : 'Pages'}`
: '',
[pageCount]
);
const dateLabel = useMemo(() => {
const d = new Date(file.lastModified);
if (Number.isNaN(d.getTime())) return '';
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: '2-digit',
year: 'numeric',
}).format(d);
}, [file.lastModified]);
// ---- Drag & drop wiring ----
const fileElementRef = useCallback((element: HTMLDivElement | null) => {
if (!element) return;
dragElementRef.current = element;
const dragCleanup = draggable({
element,
getInitialData: () => ({
type: 'file',
fileId: file.id,
fileName: file.name,
selectedFiles: [file.id] // Always drag only this file, ignore selection state
}),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
}
});
const dropCleanup = dropTargetForElements({
element,
getData: () => ({
type: 'file',
fileId: file.id
}),
canDrop: ({ source }) => {
const sourceData = source.data;
return sourceData.type === 'file' && sourceData.fileId !== file.id;
},
onDrop: ({ source }) => {
const sourceData = source.data;
if (sourceData.type === 'file' && onReorderFiles) {
const sourceFileId = sourceData.fileId as FileId;
const selectedFileIds = sourceData.selectedFiles as FileId[];
onReorderFiles(sourceFileId, file.id, selectedFileIds);
}
}
});
return () => {
dragCleanup();
dropCleanup();
};
}, [file.id, file.name, selectedFiles, onReorderFiles]);
// Update dropdown width on resize
useEffect(() => {
const update = () => {
if (dragElementRef.current) setActionsWidth(dragElementRef.current.offsetWidth);
};
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
// Close the actions dropdown when hovering outside this file card (and its dropdown)
useEffect(() => {
if (!showActions) return;
const isInsideCard = (target: EventTarget | null) => {
const container = dragElementRef.current;
if (!container) return false;
return target instanceof Node && container.contains(target);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isInsideCard(e.target)) {
setShowActions(false);
}
};
const handleTouchStart = (e: TouchEvent) => {
// On touch devices, close if the touch target is outside the card
if (!isInsideCard(e.target)) {
setShowActions(false);
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('touchstart', handleTouchStart, { passive: true });
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('touchstart', handleTouchStart);
};
}, [showActions]);
// ---- Card interactions ----
const handleCardClick = () => {
if (!isSupported) return;
onToggleFile(file.id);
};
return (
<div
ref={fileElementRef}
data-file-id={file.id}
data-testid="file-thumbnail"
data-selected={isSelected}
data-supported={isSupported}
className={`${styles.card} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative`}
style={{
opacity: isSupported ? (isDragging ? 0.9 : 1) : 0.5,
filter: isSupported ? 'none' : 'grayscale(50%)',
}}
tabIndex={0}
role="listitem"
aria-selected={isSelected}
onClick={handleCardClick}
>
{/* Header bar */}
<div
className={`${styles.header} ${
isSelected ? styles.headerSelected : styles.headerResting
}`}
>
{/* Logo/checkbox area */}
<div className={styles.logoMark}>
{isSupported ? (
<CheckboxIndicator
checked={isSelected}
onChange={() => onToggleFile(file.id)}
color="var(--checkbox-checked-bg)"
/>
) : (
<div className={styles.unsupportedPill}>
<span>
{t('unsupported', 'Unsupported')}
</span>
</div>
)}
</div>
{/* Centered index */}
<div className={styles.headerIndex} aria-label={`Position ${index + 1}`}>
{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>
</div>
{/* Actions overlay */}
{showActions && (
<div
className={styles.actionsOverlay}
style={{ width: actionsWidth }}
onClick={(e) => e.stopPropagation()}
>
<button
className={styles.actionRow}
onClick={() => {
if (actualFile) {
if (isPinned) {
unpinFile(actualFile);
onSetStatus?.(`Unpinned ${file.name}`);
} else {
pinFile(actualFile);
onSetStatus?.(`Pinned ${file.name}`);
}
}
setShowActions(false);
}}
>
{isPinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
<span>{isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}</span>
</button>
<button
className={styles.actionRow}
onClick={() => { downloadSelectedFile(); setShowActions(false); }}
>
<DownloadOutlinedIcon fontSize="small" />
<span>{t('download', 'Download')}</span>
</button>
<div className={styles.actionsDivider} />
<button
className={`${styles.actionRow} ${styles.actionDanger}`}
onClick={() => {
onDeleteFile(file.id);
onSetStatus(`Deleted ${file.name}`);
setShowActions(false);
}}
>
<DeleteOutlineIcon fontSize="small" />
<span>{t('delete', 'Delete')}</span>
</button>
</div>
)}
{/* Title + meta line */}
<div
style={{
padding: '0.5rem',
textAlign: 'center',
background: 'var(--file-card-bg)',
marginTop: '0.5rem',
marginBottom: '0.5rem',
}}>
<Text size="lg" fw={700} className={styles.title} lineClamp={2}>
{file.name}
</Text>
<Text
size="sm"
c="dimmed"
className={styles.meta}
lineClamp={3}
title={`${extUpper || 'FILE'} • ${prettySize}`}
>
{/* e.g., v2 - Jan 29, 2025 - PDF file - 3 Pages */}
{`v${file.versionNumber} - `}
{dateLabel}
{extUpper ? ` - ${extUpper} file` : ''}
{pageLabel ? ` - ${pageLabel}` : ''}
</Text>
</div>
{/* Preview area */}
<div className={`${styles.previewBox} mx-6 mb-4 relative flex-1`}>
<div className={styles.previewPaper}>
{file.thumbnailUrl && (
<img
src={file.thumbnailUrl}
alt={file.name}
draggable={false}
loading="lazy"
decoding="async"
onError={(e) => {
const img = e.currentTarget;
img.style.display = 'none';
img.parentElement?.setAttribute('data-thumb-missing', 'true');
}}
style={{
maxWidth: '80%',
maxHeight: '80%',
objectFit: 'contain',
borderRadius: 0,
background: '#ffffff',
border: '1px solid var(--border-default)',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
alignSelf: 'start'
}}
/>
)}
</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" />
</span>
{/* Tool chain display at bottom */}
{file.toolHistory && (
<div style={{
position: 'absolute',
bottom: '4px',
left: '4px',
right: '4px',
padding: '4px 6px',
textAlign: 'center',
fontWeight: 600,
overflow: 'hidden',
whiteSpace: 'nowrap'
}}>
<ToolChain
toolChain={file.toolHistory}
displayStyle="text"
size="xs"
maxWidth={'100%'}
color='var(--mantine-color-gray-7)'
/>
</div>
)}
</div>
</div>
);
};
export default React.memo(FileEditorThumbnail);
@@ -5,12 +5,12 @@ import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useTranslation } from 'react-i18next';
import { getFileSize } from '../../utils/fileUtils';
import { FileWithUrl } from '../../types/file';
import { StirlingFileStub } from '../../types/fileContext';
interface CompactFileDetailsProps {
currentFile: FileWithUrl | null;
currentFile: StirlingFileStub | null;
thumbnail: string | null;
selectedFiles: FileWithUrl[];
selectedFiles: StirlingFileStub[];
currentFileIndex: number;
numberOfFiles: number;
isAnimating: boolean;
@@ -72,12 +72,19 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
<Text size="xs" c="dimmed">
{currentFile ? getFileSize(currentFile) : ''}
{selectedFiles.length > 1 && ` • ${selectedFiles.length} files`}
{currentFile && ` • v${currentFile.versionNumber || 1}`}
</Text>
{hasMultipleFiles && (
<Text size="xs" c="blue">
{currentFileIndex + 1} of {selectedFiles.length}
</Text>
)}
{/* Compact tool chain for mobile */}
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
<Text size="xs" c="dimmed">
{currentFile.toolHistory.map((tool: any) => tool.toolName).join(' → ')}
</Text>
)}
</Box>
{/* Navigation arrows for multiple files */}
@@ -4,6 +4,7 @@ import FileSourceButtons from './FileSourceButtons';
import FileDetails from './FileDetails';
import SearchInput from './SearchInput';
import FileListArea from './FileListArea';
import FileActions from './FileActions';
import HiddenFileInput from './HiddenFileInput';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
@@ -17,27 +18,27 @@ const DesktopLayout: React.FC = () => {
return (
<Grid gutter="xs" h="100%" grow={false} style={{ flexWrap: 'nowrap', minWidth: 0 }}>
{/* Column 1: File Sources */}
<Grid.Col span="content" p="lg" style={{
minWidth: '13.625rem',
width: '13.625rem',
flexShrink: 0,
<Grid.Col span="content" p="lg" style={{
minWidth: '13.625rem',
width: '13.625rem',
flexShrink: 0,
height: '100%',
}}>
<FileSourceButtons />
</Grid.Col>
{/* Column 2: File List */}
<Grid.Col span="auto" style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
<Grid.Col span="auto" style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: 0,
minWidth: 0,
flex: '1 1 0px'
}}>
<div style={{
flex: 1,
display: 'flex',
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
border: '1px solid var(--mantine-color-gray-2)',
@@ -45,18 +46,26 @@ const DesktopLayout: React.FC = () => {
overflow: 'hidden'
}}>
{activeSource === 'recent' && (
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<SearchInput />
</div>
<>
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<SearchInput />
</div>
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<FileActions />
</div>
</>
)}
<div style={{ flex: 1, minHeight: 0 }}>
<FileListArea
scrollAreaHeight={`calc(${modalHeight} )`}
scrollAreaStyle={{
scrollAreaStyle={{
height: activeSource === 'recent' && recentFiles.length > 0 ? modalHeight : '100%',
backgroundColor: 'transparent',
border: 'none',
@@ -66,12 +75,12 @@ const DesktopLayout: React.FC = () => {
</div>
</div>
</Grid.Col>
{/* Column 3: File Details */}
<Grid.Col p="xl" span="content" style={{
minWidth: '25rem',
width: '25rem',
flexShrink: 0,
<Grid.Col p="xl" span="content" style={{
minWidth: '25rem',
width: '25rem',
flexShrink: 0,
height: '100%',
maxWidth: '18rem'
}}>
@@ -79,11 +88,11 @@ const DesktopLayout: React.FC = () => {
<FileDetails />
</div>
</Grid.Col>
{/* Hidden file input for local file selection */}
<HiddenFileInput />
</Grid>
);
};
export default DesktopLayout;
export default DesktopLayout;
@@ -0,0 +1,115 @@
import React from "react";
import { Group, Text, ActionIcon, Tooltip } from "@mantine/core";
import SelectAllIcon from "@mui/icons-material/SelectAll";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "../../contexts/FileManagerContext";
const FileActions: React.FC = () => {
const { t } = useTranslation();
const { recentFiles, selectedFileIds, filteredFiles, onSelectAll, onDeleteSelected, onDownloadSelected } =
useFileManagerContext();
const handleSelectAll = () => {
onSelectAll();
};
const handleDeleteSelected = () => {
if (selectedFileIds.length > 0) {
onDeleteSelected();
}
};
const handleDownloadSelected = () => {
if (selectedFileIds.length > 0) {
onDownloadSelected();
}
};
// Only show actions if there are files
if (recentFiles.length === 0) {
return null;
}
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
const hasSelection = selectedFileIds.length > 0;
return (
<div
style={{
padding: "0.75rem 1rem",
backgroundColor: "var(--mantine-color-gray-1)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
minHeight: "3rem",
position: "relative",
}}
>
{/* Left: Select All */}
<div>
<Tooltip
label={allFilesSelected ? t("fileManager.deselectAll", "Deselect All") : t("fileManager.selectAll", "Select All")}
>
<ActionIcon
variant="light"
size="sm"
color="dimmed"
onClick={handleSelectAll}
disabled={filteredFiles.length === 0}
radius="sm"
>
<SelectAllIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
</Tooltip>
</div>
{/* Center: Selected count */}
<div
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
}}
>
{hasSelection && (
<Text size="sm" c="dimmed" fw={500}>
{t("fileManager.selectedCount", "{{count}} selected", { count: selectedFileIds.length })}
</Text>
)}
</div>
{/* Right: Delete and Download */}
<Group gap="xs">
<Tooltip label={t("fileManager.deleteSelected", "Delete Selected")}>
<ActionIcon
variant="light"
size="sm"
color="dimmed"
onClick={handleDeleteSelected}
disabled={!hasSelection}
radius="sm"
>
<DeleteIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
</Tooltip>
<Tooltip label={t("fileManager.downloadSelected", "Download Selected")}>
<ActionIcon
variant="light"
size="sm"
color="dimmed"
onClick={handleDownloadSelected}
disabled={!hasSelection}
radius="sm"
>
<DownloadIcon style={{ fontSize: "1rem" }} />
</ActionIcon>
</Tooltip>
</Group>
</div>
);
};
export default FileActions;
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { Stack, Button, Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
@@ -11,27 +11,26 @@ interface FileDetailsProps {
compact?: boolean;
}
const FileDetails: React.FC<FileDetailsProps> = ({
const FileDetails: React.FC<FileDetailsProps> = ({
compact = false
}) => {
const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext();
const { t } = useTranslation();
const [currentFileIndex, setCurrentFileIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
// Get the currently displayed file
const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null;
const hasSelection = selectedFiles.length > 0;
const hasMultipleFiles = selectedFiles.length > 1;
// Use IndexedDB hook for the current file
const { thumbnail: currentThumbnail } = useIndexedDBThumbnail(currentFile);
// Get thumbnail for current file
const getCurrentThumbnail = () => {
return currentThumbnail;
};
const handlePrevious = () => {
if (isAnimating) return;
setIsAnimating(true);
@@ -40,7 +39,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
setIsAnimating(false);
}, 150);
};
const handleNext = () => {
if (isAnimating) return;
setIsAnimating(true);
@@ -49,14 +48,14 @@ const FileDetails: React.FC<FileDetailsProps> = ({
setIsAnimating(false);
}, 150);
};
// Reset index when selection changes
React.useEffect(() => {
if (currentFileIndex >= selectedFiles.length) {
setCurrentFileIndex(0);
}
}, [selectedFiles.length, currentFileIndex]);
if (compact) {
return (
<CompactFileDetails
@@ -88,26 +87,26 @@ const FileDetails: React.FC<FileDetailsProps> = ({
onNext={handleNext}
/>
</Box>
{/* Section 2: File Details */}
<FileInfoCard
currentFile={currentFile}
modalHeight={modalHeight}
/>
<Button
size="md"
<Button
size="md"
mb="xl"
onClick={onOpenFiles}
disabled={!hasSelection}
fullWidth
style={{
flexShrink: 0,
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
style={{
flexShrink: 0,
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
}}
>
{selectedFiles.length > 1
{selectedFiles.length > 1
? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`)
: t('fileManager.openFile', 'Open File')
}
@@ -116,4 +115,4 @@ const FileDetails: React.FC<FileDetailsProps> = ({
);
};
export default FileDetails;
export default FileDetails;
@@ -0,0 +1,68 @@
import React from 'react';
import { Box, Text, Collapse, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { StirlingFileStub } from '../../types/fileContext';
import FileListItem from './FileListItem';
interface FileHistoryGroupProps {
leafFile: StirlingFileStub;
historyFiles: StirlingFileStub[];
isExpanded: boolean;
onDownloadSingle: (file: StirlingFileStub) => void;
onFileDoubleClick: (file: StirlingFileStub) => void;
onHistoryFileRemove: (file: StirlingFileStub) => void;
isFileSupported: (fileName: string) => boolean;
}
const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
leafFile,
historyFiles,
isExpanded,
onDownloadSingle,
onFileDoubleClick,
onHistoryFileRemove,
isFileSupported,
}) => {
const { t } = useTranslation();
// Sort history files by version number (oldest first, excluding the current leaf file)
const sortedHistory = historyFiles
.filter(file => file.id !== leafFile.id) // Exclude the leaf file itself
.sort((a, b) => (b.versionNumber || 1) - (a.versionNumber || 1));
if (!isExpanded || sortedHistory.length === 0) {
return null;
}
return (
<Collapse in={isExpanded}>
<Box ml="md" mt="xs" mb="sm">
<Group align="center" mb="sm">
<Text size="xs" fw={600} c="dimmed">
{t('fileManager.fileHistory', 'File History')} ({sortedHistory.length})
</Text>
</Group>
<Box ml="md">
{sortedHistory.map((historyFile, _index) => (
<FileListItem
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
file={historyFile}
isSelected={false} // History files are not selectable
isSupported={isFileSupported(historyFile.name)}
onSelect={() => {}} // No selection for history files
onRemove={() => onHistoryFileRemove(historyFile)} // Remove specific history file
onDownload={() => onDownloadSingle(historyFile)}
onDoubleClick={() => onFileDoubleClick(historyFile)}
isHistoryFile={true} // This enables "Add to Recents" in menu
isLatestVersion={false} // History files are never latest
// onAddToRecents is accessed from context by FileListItem
/>
))}
</Box>
</Box>
</Collapse>
);
};
export default FileHistoryGroup;
@@ -2,10 +2,11 @@ import React from 'react';
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { detectFileExtension, getFileSize } from '../../utils/fileUtils';
import { FileWithUrl } from '../../types/file';
import { StirlingFileStub } from '../../types/fileContext';
import ToolChain from '../shared/ToolChain';
interface FileInfoCardProps {
currentFile: FileWithUrl | null;
currentFile: StirlingFileStub | null;
modalHeight: string;
}
@@ -17,7 +18,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
return (
<Card withBorder p={0} h={`calc(${modalHeight} * 0.32 - 1rem)`} style={{ flex: 1, overflow: 'hidden' }}>
<Box bg="blue.6" p="sm" style={{ borderTopLeftRadius: 'var(--mantine-radius-md)', borderTopRightRadius: 'var(--mantine-radius-md)' }}>
<Box bg="gray.4" p="sm" style={{ borderTopLeftRadius: 'var(--mantine-radius-md)', borderTopRightRadius: 'var(--mantine-radius-md)' }}>
<Text size="sm" fw={500} ta="center" c="white">
{t('fileManager.details', 'File Details')}
</Text>
@@ -31,7 +32,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
</Text>
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileFormat', 'Format')}</Text>
{currentFile ? (
@@ -43,7 +44,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
)}
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileSize', 'Size')}</Text>
<Text size="sm" fw={500}>
@@ -51,17 +52,42 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
</Text>
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileVersion', 'Version')}</Text>
<Text size="sm" c="dimmed">{t('fileManager.lastModified', 'Last Modified')}</Text>
<Text size="sm" fw={500}>
{currentFile ? '1.0' : ''}
{currentFile ? new Date(currentFile.lastModified).toLocaleDateString() : ''}
</Text>
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileVersion', 'Version')}</Text>
{currentFile &&
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? 'blue' : 'gray'}>
v{currentFile ? (currentFile.versionNumber || 1) : ''}
</Badge>}
</Group>
{/* Tool Chain Display */}
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
<>
<Divider />
<Box py="xs">
<Text size="xs" c="dimmed" mb="xs">{t('fileManager.toolChain', 'Tools Applied')}</Text>
<ToolChain
toolChain={currentFile.toolHistory}
displayStyle="badges"
size="xs"
/>
</Box>
</>
)}
</Stack>
</ScrollArea>
</Card>
);
};
export default FileInfoCard;
export default FileInfoCard;
@@ -4,6 +4,7 @@ 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 { useFileManagerContext } from '../../contexts/FileManagerContext';
interface FileListAreaProps {
@@ -19,22 +20,26 @@ const FileListArea: React.FC<FileListAreaProps> = ({
activeSource,
recentFiles,
filteredFiles,
selectedFileIds,
selectedFilesSet,
expandedFileIds,
loadedHistoryFiles,
onFileSelect,
onFileRemove,
onHistoryFileRemove,
onFileDoubleClick,
onDownloadSingle,
isFileSupported,
} = useFileManagerContext();
const { t } = useTranslation();
if (activeSource === 'recent') {
return (
<ScrollArea
<ScrollArea
h={scrollAreaHeight}
style={{
style={{
...scrollAreaStyle
}}
type="always"
type="always"
scrollbarSize={8}
>
<Stack gap={0}>
@@ -49,17 +54,37 @@ const FileListArea: React.FC<FileListAreaProps> = ({
</Stack>
</Center>
) : (
filteredFiles.map((file, index) => (
<FileListItem
key={file.id || file.name}
file={file}
isSelected={selectedFileIds.includes(file.id || file.name)}
isSupported={isFileSupported(file.name)}
onSelect={() => onFileSelect(file)}
onRemove={() => onFileRemove(index)}
onDoubleClick={() => onFileDoubleClick(file)}
/>
))
filteredFiles.map((file, index) => {
// All files in filteredFiles are now leaf files only
const historyFiles = loadedHistoryFiles.get(file.id) || [];
const isExpanded = expandedFileIds.has(file.id);
return (
<React.Fragment key={file.id}>
<FileListItem
file={file}
isSelected={selectedFilesSet.has(file.id)}
isSupported={isFileSupported(file.name)}
onSelect={(shiftKey) => onFileSelect(file, index, shiftKey)}
onRemove={() => onFileRemove(index)}
onDownload={() => onDownloadSingle(file)}
onDoubleClick={() => onFileDoubleClick(file)}
isHistoryFile={false} // All files here are leaf files
isLatestVersion={true} // All files here are the latest versions
/>
<FileHistoryGroup
leafFile={file}
historyFiles={historyFiles}
isExpanded={isExpanded}
onDownloadSingle={onDownloadSingle}
onFileDoubleClick={onFileDoubleClick}
onHistoryFileRemove={onHistoryFileRemove}
isFileSupported={isFileSupported}
/>
</React.Fragment>
);
})
)}
</Stack>
</ScrollArea>
@@ -77,4 +102,4 @@ const FileListArea: React.FC<FileListAreaProps> = ({
);
};
export default FileListArea;
export default FileListArea;
@@ -1,79 +1,208 @@
import React, { useState } from 'react';
import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge } from '@mantine/core';
import MoreVertIcon from '@mui/icons-material/MoreVert';
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 { useTranslation } from 'react-i18next';
import { getFileSize, getFileDate } from '../../utils/fileUtils';
import { FileWithUrl } from '../../types/file';
import { FileId, StirlingFileStub } from '../../types/fileContext';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import ToolChain from '../shared/ToolChain';
interface FileListItemProps {
file: FileWithUrl;
file: StirlingFileStub;
isSelected: boolean;
isSupported: boolean;
onSelect: () => void;
onSelect: (shiftKey?: boolean) => void;
onRemove: () => void;
onDownload?: () => void;
onDoubleClick?: () => void;
isLast?: boolean;
isHistoryFile?: boolean; // Whether this is a history file (indented)
isLatestVersion?: boolean; // Whether this is the latest version (shows chevron)
}
const FileListItem: React.FC<FileListItemProps> = ({
file,
isSelected,
isSupported,
onSelect,
onRemove,
onDoubleClick
const FileListItem: React.FC<FileListItemProps> = ({
file,
isSelected,
isSupported,
onSelect,
onRemove,
onDownload,
onDoubleClick,
isHistoryFile = false,
isLatestVersion = false
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const {expandedFileIds, onToggleExpansion, onAddToRecents } = useFileManagerContext();
// Keep item in hovered state if menu is open
const shouldShowHovered = isHovered || isMenuOpen;
// Get version information for this file
const leafFileId = (isLatestVersion ? file.id : (file.originalFileId || file.id)) as FileId;
const hasVersionHistory = (file.versionNumber || 1) > 1; // Show history for any processed file (v2+)
const currentVersion = file.versionNumber || 1; // Display original files as v1
const isExpanded = expandedFileIds.has(leafFileId);
return (
<>
<Box
p="sm"
style={{
cursor: 'pointer',
backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (isHovered ? 'var(--mantine-color-gray-0)' : 'var(--bg-file-list)'),
<Box
p="sm"
style={{
cursor: isHistoryFile ? 'default' : 'pointer',
backgroundColor: isSelected
? 'var(--mantine-color-gray-1)'
: (shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'var(--bg-file-list)'),
opacity: isSupported ? 1 : 0.5,
transition: 'background-color 0.15s ease'
transition: 'background-color 0.15s ease',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
paddingLeft: isHistoryFile ? '2rem' : '0.75rem', // Indent history files
borderLeft: isHistoryFile ? '3px solid var(--mantine-color-blue-4)' : 'none' // Visual indicator for history
}}
onClick={onSelect}
onClick={isHistoryFile ? undefined : (e) => onSelect(e.shiftKey)}
onDoubleClick={onDoubleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Group gap="sm">
<Box>
<Checkbox
checked={isSelected}
onChange={() => {}} // Handled by parent onClick
size="sm"
pl="sm"
pr="xs"
styles={{
input: {
cursor: 'pointer'
}
}}
/>
</Box>
{!isHistoryFile && (
<Box>
{/* Checkbox for regular files only */}
<Checkbox
checked={isSelected}
onChange={() => {}} // Handled by parent onClick
size="sm"
pl="sm"
pr="xs"
styles={{
input: {
cursor: 'pointer'
}
}}
/>
</Box>
)}
<Box style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={500} truncate>{file.name}</Text>
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
<Group gap="xs" align="center">
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>{file.name}</Text>
<Badge size="xs" variant="light" color={"blue"}>
v{currentVersion}
</Badge>
</Group>
<Group gap="xs" align="center">
<Text size="xs" c="dimmed">
{getFileSize(file)} • {getFileDate(file)}
</Text>
{/* Tool chain for processed files */}
{file.toolHistory && file.toolHistory.length > 0 && (
<ToolChain
toolChain={file.toolHistory}
maxWidth={'150px'}
displayStyle="text"
size="xs"
/>
)}
</Group>
</Box>
{/* Delete button - fades in/out on hover */}
<ActionIcon
variant="subtle"
c="dimmed"
size="md"
onClick={(e) => { e.stopPropagation(); onRemove(); }}
style={{
opacity: isHovered ? 1 : 0,
transform: isHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: isHovered ? 'auto' : 'none'
}}
{/* Three dots menu - fades in/out on hover */}
<Menu
position="bottom-end"
withinPortal
onOpen={() => setIsMenuOpen(true)}
onClose={() => setIsMenuOpen(false)}
>
<DeleteIcon style={{ fontSize: 20 }} />
</ActionIcon>
<Menu.Target>
<ActionIcon
variant="subtle"
c="dimmed"
size="md"
onClick={(e) => e.stopPropagation()}
style={{
opacity: shouldShowHovered ? 1 : 0,
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: shouldShowHovered ? 'auto' : 'none'
}}
>
<MoreVertIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{onDownload && (
<Menu.Item
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onDownload();
}}
>
{t('fileManager.download', 'Download')}
</Menu.Item>
)}
{/* Show/Hide History option for latest version files */}
{isLatestVersion && hasVersionHistory && (
<>
<Menu.Item
leftSection={
<HistoryIcon style={{ fontSize: 16 }} />
}
onClick={(e) => {
e.stopPropagation();
onToggleExpansion(leafFileId);
}}
>
{
(isExpanded ?
t('fileManager.hideHistory', 'Hide History') :
t('fileManager.showHistory', 'Show History')
)
}
</Menu.Item>
<Menu.Divider />
</>
)}
{/* Restore option for history files */}
{isHistoryFile && (
<>
<Menu.Item
leftSection={<RestoreIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onAddToRecents(file);
}}
>
{t('fileManager.restore', 'Restore')}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
>
{t('fileManager.delete', 'Delete')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Box>
{ <Divider color="var(--mantine-color-gray-3)" />}
@@ -81,4 +210,4 @@ const FileListItem: React.FC<FileListItemProps> = ({
);
};
export default FileListItem;
export default FileListItem;
@@ -1,7 +1,7 @@
import React from 'react';
import { Stack, Text, Button, Group } from '@mantine/core';
import HistoryIcon from '@mui/icons-material/History';
import FolderIcon from '@mui/icons-material/Folder';
import UploadIcon from '@mui/icons-material/Upload';
import CloudIcon from '@mui/icons-material/Cloud';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
@@ -10,7 +10,7 @@ interface FileSourceButtonsProps {
horizontal?: boolean;
}
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
horizontal = false
}) => {
const { activeSource, onSourceChange, onLocalFileClick } = useFileManagerContext();
@@ -44,11 +44,11 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
>
{horizontal ? t('fileManager.recent', 'Recent') : t('fileManager.recent', 'Recent')}
</Button>
<Button
variant="subtle"
color='var(--mantine-color-gray-6)'
leftSection={<FolderIcon />}
leftSection={<UploadIcon />}
justify={horizontal ? "center" : "flex-start"}
onClick={onLocalFileClick}
fullWidth={!horizontal}
@@ -63,9 +63,9 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
}
}}
>
{horizontal ? t('fileManager.localFiles', 'Local') : t('fileManager.localFiles', 'Local Files')}
{horizontal ? t('fileUpload.uploadFiles', 'Upload') : t('fileUpload.uploadFiles', 'Upload Files')}
</Button>
<Button
variant={buttonProps.variant('drive')}
leftSection={<CloudIcon />}
@@ -100,4 +100,4 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
);
};
export default FileSourceButtons;
export default FileSourceButtons;
@@ -1,9 +1,10 @@
import React from 'react';
import { Stack, Box } from '@mantine/core';
import { Box } from '@mantine/core';
import FileSourceButtons from './FileSourceButtons';
import FileDetails from './FileDetails';
import SearchInput from './SearchInput';
import FileListArea from './FileListArea';
import FileActions from './FileActions';
import HiddenFileInput from './HiddenFileInput';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
@@ -18,14 +19,15 @@ const MobileLayout: React.FC = () => {
const calculateFileListHeight = () => {
// Base modal height minus padding and gaps
const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding
// Estimate heights of fixed components
const fileSourceHeight = '3rem'; // FileSourceButtons height
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom)
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`;
const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`;
};
return (
@@ -34,15 +36,15 @@ const MobileLayout: React.FC = () => {
<Box style={{ flexShrink: 0 }}>
<FileSourceButtons horizontal={true} />
</Box>
<Box style={{ flexShrink: 0 }}>
<FileDetails compact={true} />
</Box>
{/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */}
<Box style={{
<Box style={{
flex: 1,
display: 'flex',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
borderRadius: '0.5rem',
@@ -51,18 +53,26 @@ const MobileLayout: React.FC = () => {
minHeight: 0
}}>
{activeSource === 'recent' && (
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<SearchInput />
</Box>
<>
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<SearchInput />
</Box>
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<FileActions />
</Box>
</>
)}
<Box style={{ flex: 1, minHeight: 0 }}>
<FileListArea
scrollAreaHeight={calculateFileListHeight()}
scrollAreaStyle={{
scrollAreaStyle={{
height: calculateFileListHeight(),
maxHeight: '60vh',
minHeight: '9.375rem',
@@ -73,11 +83,11 @@ const MobileLayout: React.FC = () => {
/>
</Box>
</Box>
{/* Hidden file input for local file selection */}
<HiddenFileInput />
</Box>
);
};
export default MobileLayout;
export default MobileLayout;
@@ -1,178 +0,0 @@
import React from 'react';
import {
Stack,
Paper,
Text,
Badge,
Group,
Collapse,
Box,
ScrollArea,
Code,
Divider
} from '@mantine/core';
import { useFileContext } from '../../contexts/FileContext';
import { FileOperation, FileOperationHistory as FileOperationHistoryType } from '../../types/fileContext';
import { PageOperation } from '../../types/pageEditor';
interface FileOperationHistoryProps {
fileId: string;
showOnlyApplied?: boolean;
maxHeight?: number;
}
const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
fileId,
showOnlyApplied = false,
maxHeight = 400
}) => {
const { getFileHistory, getAppliedOperations } = useFileContext();
const history = getFileHistory(fileId);
const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
const operations = allOperations.filter(op => 'fileIds' in op) as FileOperation[];
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};
const getOperationIcon = (type: string) => {
switch (type) {
case 'split': return '✂️';
case 'merge': return '🔗';
case 'compress': return '🗜️';
case 'rotate': return '🔄';
case 'delete': return '🗑️';
case 'move': return '↕️';
case 'insert': return '📄';
case 'upload': return '⬆️';
case 'add': return '➕';
case 'remove': return '➖';
case 'replace': return '🔄';
case 'convert': return '🔄';
default: return '⚙️';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'applied': return 'green';
case 'failed': return 'red';
case 'pending': return 'yellow';
default: return 'gray';
}
};
const renderOperationDetails = (operation: FileOperation) => {
if ('metadata' in operation && operation.metadata) {
const { metadata } = operation;
return (
<Box mt="xs">
{metadata.parameters && (
<Text size="xs" c="dimmed">
Parameters: <Code>{JSON.stringify(metadata.parameters, null, 2)}</Code>
</Text>
)}
{metadata.originalFileName && (
<Text size="xs" c="dimmed">
Original file: {metadata.originalFileName}
</Text>
)}
{metadata.outputFileNames && (
<Text size="xs" c="dimmed">
Output files: {metadata.outputFileNames.join(', ')}
</Text>
)}
{metadata.fileSize && (
<Text size="xs" c="dimmed">
File size: {(metadata.fileSize / 1024 / 1024).toFixed(2)} MB
</Text>
)}
{metadata.pageCount && (
<Text size="xs" c="dimmed">
Pages: {metadata.pageCount}
</Text>
)}
{metadata.error && (
<Text size="xs" c="red">
Error: {metadata.error}
</Text>
)}
</Box>
);
}
return null;
};
if (!history || operations.length === 0) {
return (
<Paper p="md" withBorder>
<Text c="dimmed" ta="center">
{showOnlyApplied ? 'No applied operations found' : 'No operation history available'}
</Text>
</Paper>
);
}
return (
<Paper p="md" withBorder>
<Group justify="space-between" mb="md">
<Text fw={500}>
{showOnlyApplied ? 'Applied Operations' : 'Operation History'}
</Text>
<Badge variant="light" color="blue">
{operations.length} operations
</Badge>
</Group>
<ScrollArea h={maxHeight}>
<Stack gap="sm">
{operations.map((operation, index) => (
<Paper key={operation.id} p="sm" withBorder radius="sm" bg="gray.0">
<Group justify="space-between" align="start">
<Group gap="xs">
<Text span size="lg">
{getOperationIcon(operation.type)}
</Text>
<Box>
<Text fw={500} size="sm">
{operation.type.charAt(0).toUpperCase() + operation.type.slice(1)}
</Text>
<Text size="xs" c="dimmed">
{formatTimestamp(operation.timestamp)}
</Text>
</Box>
</Group>
<Badge
variant="filled"
color={getStatusColor(operation.status)}
size="sm"
>
{operation.status}
</Badge>
</Group>
{renderOperationDetails(operation)}
{index < operations.length - 1 && <Divider mt="sm" />}
</Paper>
))}
</Stack>
</ScrollArea>
{history && (
<Group justify="space-between" mt="sm" pt="sm" style={{ borderTop: '1px solid var(--mantine-color-gray-3)' }}>
<Text size="xs" c="dimmed">
Created: {formatTimestamp(history.createdAt)}
</Text>
<Text size="xs" c="dimmed">
Last modified: {formatTimestamp(history.lastModified)}
</Text>
</Group>
)}
</Paper>
);
};
export default FileOperationHistory;
@@ -0,0 +1,21 @@
.workbench-scrollable {
overflow-y: auto !important;
overflow-x: hidden !important;
}
.workbench-scrollable::-webkit-scrollbar {
width: 0.375rem;
}
.workbench-scrollable::-webkit-scrollbar-track {
background: transparent;
}
.workbench-scrollable::-webkit-scrollbar-thumb {
background-color: var(--mantine-color-gray-4);
border-radius: 0.1875rem;
}
.workbench-scrollable::-webkit-scrollbar-thumb:hover {
background-color: var(--mantine-color-gray-5);
}
+41 -32
View File
@@ -1,26 +1,30 @@
import React from 'react';
import { Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useFileHandler } from '../../hooks/useFileHandler';
import { useFileContext } from '../../contexts/FileContext';
import { useFileState } from '../../contexts/FileContext';
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
import { useToolManagement } from '../../hooks/useToolManagement';
import './Workbench.css';
import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor';
import PageEditor from '../pageEditor/PageEditor';
import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer';
import ToolRenderer from '../tools/ToolRenderer';
import LandingPage from '../shared/LandingPage';
import Footer from '../shared/Footer';
// No props needed - component uses contexts directly
export default function Workbench() {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
// Use context-based hooks to eliminate all prop drilling
const { activeFiles, currentView, setCurrentView } = useFileContext();
const { state } = useFileState();
const { workbench: currentView } = useNavigationState();
const { actions: navActions } = useNavigationActions();
const setCurrentView = navActions.setWorkbench;
const activeFiles = state.files.ids;
const {
previewFile,
pageEditorFunctions,
@@ -30,8 +34,15 @@ export default function Workbench() {
setSidebarsVisible
} = useToolWorkflow();
const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow();
const { addToActiveFiles } = useFileHandler();
const { handleToolSelect } = useToolWorkflow();
// Get navigation state - this is the source of truth
const { selectedTool: selectedToolId } = useNavigationState();
// Get tool registry to look up selected tool
const { toolRegistry } = useToolManagement();
const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null;
const { addFiles } = useFileHandler();
const handlePreviewClose = () => {
setPreviewFile(null);
@@ -47,12 +58,12 @@ export default function Workbench() {
handleToolSelect('convert');
sessionStorage.removeItem('previousMode');
} else {
setCurrentView('fileEditor' as any);
setCurrentView('fileEditor');
}
};
const renderMainContent = () => {
if (!activeFiles[0]) {
if (activeFiles.length === 0) {
return (
<LandingPage
/>
@@ -63,17 +74,15 @@ export default function Workbench() {
case "fileEditor":
return (
<FileEditor
toolMode={!!selectedToolKey}
showUpload={true}
showBulkActions={!selectedToolKey}
toolMode={!!selectedToolId}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
{...(!selectedToolKey && {
onOpenPageEditor: (file) => {
setCurrentView("pageEditor" as any);
{...(!selectedToolId && {
onOpenPageEditor: () => {
setCurrentView("pageEditor");
},
onMergeFiles: (filesToMerge) => {
filesToMerge.forEach(addToActiveFiles);
setCurrentView("viewer" as any);
addFiles(filesToMerge);
setCurrentView("viewer");
}
})}
/>
@@ -105,25 +114,22 @@ export default function Workbench() {
onRotate={pageEditorFunctions.handleRotate}
onDelete={pageEditorFunctions.handleDelete}
onSplit={pageEditorFunctions.handleSplit}
onExportSelected={pageEditorFunctions.onExportSelected}
onSplitAll={pageEditorFunctions.handleSplitAll}
onPageBreak={pageEditorFunctions.handlePageBreak}
onPageBreakAll={pageEditorFunctions.handlePageBreakAll}
onExportAll={pageEditorFunctions.onExportAll}
exportLoading={pageEditorFunctions.exportLoading}
selectionMode={pageEditorFunctions.selectionMode}
selectedPages={pageEditorFunctions.selectedPages}
selectedPageIds={pageEditorFunctions.selectedPageIds}
displayDocument={pageEditorFunctions.displayDocument}
splitPositions={pageEditorFunctions.splitPositions}
totalPages={pageEditorFunctions.totalPages}
/>
)}
</>
);
default:
// Check if it's a tool view
if (selectedToolKey && selectedTool) {
return (
<ToolRenderer
selectedToolKey={selectedToolKey}
/>
);
}
return (
<LandingPage/>
);
@@ -132,7 +138,7 @@ export default function Workbench() {
return (
<Box
className="flex-1 h-screen min-w-80 relative flex flex-col"
className="flex-1 h-full min-w-80 relative flex flex-col"
style={
isRainbowMode
? {} // No background color in rainbow mode
@@ -142,19 +148,22 @@ export default function Workbench() {
{/* Top Controls */}
<TopControls
currentView={currentView}
setCurrentView={setCurrentView as any /* FIX ME */}
selectedToolKey={selectedToolKey}
setCurrentView={setCurrentView}
selectedToolKey={selectedToolId}
/>
{/* Main content area */}
<Box
className="flex-1 min-h-0 relative z-10"
className="flex-1 min-h-0 relative z-10 workbench-scrollable "
style={{
transition: 'opacity 0.15s ease-in-out',
marginTop: '1rem',
}}
>
{renderMainContent()}
</Box>
<Footer analyticsEnabled />
</Box>
);
}
@@ -1,21 +1,23 @@
import React from 'react';
import { Paper, Group, TextInput, Button, Text } from '@mantine/core';
import { Group, TextInput, Button, Text } from '@mantine/core';
interface BulkSelectionPanelProps {
csvInput: string;
setCsvInput: (value: string) => void;
selectedPages: number[];
selectedPageIds: string[];
displayDocument?: { pages: { id: string; pageNumber: number }[] };
onUpdatePagesFromCSV: () => void;
}
const BulkSelectionPanel = ({
csvInput,
setCsvInput,
selectedPages,
selectedPageIds,
displayDocument,
onUpdatePagesFromCSV,
}: BulkSelectionPanelProps) => {
return (
<Paper p="md" mb="md" withBorder>
<>
<Group>
<TextInput
value={csvInput}
@@ -30,12 +32,15 @@ const BulkSelectionPanel = ({
Apply
</Button>
</Group>
{selectedPages.length > 0 && (
{selectedPageIds.length > 0 && (
<Text size="sm" c="dimmed" mt="sm">
Selected: {selectedPages.length} pages
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
const page = displayDocument.pages.find(p => p.id === id);
return page?.pageNumber || 0;
}).filter(n => n > 0).join(', ') : ''})
</Text>
)}
</Paper>
</>
);
};
@@ -1,135 +1,156 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Box } from '@mantine/core';
import styles from './PageEditor.module.css';
import { useVirtualizer } from '@tanstack/react-virtual';
import { GRID_CONSTANTS } from './constants';
interface DragDropItem {
id: string;
splitBefore?: boolean;
splitAfter?: boolean;
}
interface DragDropGridProps<T extends DragDropItem> {
items: T[];
selectedItems: number[];
selectedItems: string[];
selectionMode: boolean;
isAnimating: boolean;
onDragStart: (pageNumber: number) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent) => void;
onDragEnter: (pageNumber: number) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, targetPageNumber: number | 'end') => void;
onEndZoneDragEnter: () => void;
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
draggedItem: number | null;
dropTarget: number | 'end' | null;
multiItemDrag: {pageNumbers: number[], count: number} | null;
dragPosition: {x: number, y: number} | null;
}
const DragDropGrid = <T extends DragDropItem>({
items,
selectedItems,
selectionMode,
isAnimating,
onDragStart,
onDragEnd,
onDragOver,
onDragEnter,
onDragLeave,
onDrop,
onEndZoneDragEnter,
renderItem,
renderSplitMarker,
draggedItem,
dropTarget,
multiItemDrag,
dragPosition,
}: DragDropGridProps<T>) => {
const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const containerRef = useRef<HTMLDivElement>(null);
// Global drag cleanup
// Responsive grid configuration
const [itemsPerRow, setItemsPerRow] = useState(4);
const OVERSCAN = items.length > 1000 ? GRID_CONSTANTS.OVERSCAN_LARGE : GRID_CONSTANTS.OVERSCAN_SMALL;
// Calculate items per row based on container width
const calculateItemsPerRow = useCallback(() => {
if (!containerRef.current) return 4; // Default fallback
const containerWidth = containerRef.current.offsetWidth;
if (containerWidth === 0) return 4; // Container not measured yet
// Convert rem to pixels for calculation
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
const ITEM_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
// Calculate how many items fit: (width - gap) / (itemWidth + gap)
const availableWidth = containerWidth - ITEM_GAP; // Account for first gap
const itemWithGap = ITEM_WIDTH + ITEM_GAP;
const calculated = Math.floor(availableWidth / itemWithGap);
return Math.max(1, calculated); // At least 1 item per row
}, []);
// Update items per row when container resizes
useEffect(() => {
const handleGlobalDragEnd = () => {
onDragEnd();
const updateLayout = () => {
const newItemsPerRow = calculateItemsPerRow();
setItemsPerRow(newItemsPerRow);
};
const handleGlobalDrop = (e: DragEvent) => {
e.preventDefault();
};
// Initial calculation
updateLayout();
if (draggedItem) {
document.addEventListener('dragend', handleGlobalDragEnd);
document.addEventListener('drop', handleGlobalDrop);
// Listen for window resize
window.addEventListener('resize', updateLayout);
// Use ResizeObserver for container size changes
const resizeObserver = new ResizeObserver(updateLayout);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => {
document.removeEventListener('dragend', handleGlobalDragEnd);
document.removeEventListener('drop', handleGlobalDrop);
window.removeEventListener('resize', updateLayout);
resizeObserver.disconnect();
};
}, [draggedItem, onDragEnd]);
}, [calculateItemsPerRow]);
// Virtualization with react-virtual library
const rowVirtualizer = useVirtualizer({
count: Math.ceil(items.length / itemsPerRow),
getScrollElement: () => containerRef.current?.closest('[data-scrolling-container]') as Element,
estimateSize: () => {
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
return parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx;
},
overscan: OVERSCAN,
});
// Calculate optimal width for centering
const remToPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
const itemWidth = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
const itemGap = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
const gridWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * itemGap;
return (
<Box>
<Box
ref={containerRef}
style={{
// Basic container styles
width: '100%',
height: '100%',
}}
>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '1.5rem',
justifyContent: 'flex-start',
paddingBottom: '100px',
// Performance optimizations for smooth scrolling
willChange: 'scroll-position',
transform: 'translateZ(0)', // Force hardware acceleration
backfaceVisibility: 'hidden',
// Use containment for better rendering performance
contain: 'layout style paint',
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
margin: '0 auto',
maxWidth: `${gridWidth}px`,
}}
>
{items.map((item, index) => (
<React.Fragment key={item.id}>
{/* Split marker */}
{renderSplitMarker && item.splitBefore && index > 0 && renderSplitMarker(item, index)}
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * itemsPerRow;
const endIndex = Math.min(startIndex + itemsPerRow, items.length);
const rowItems = items.slice(startIndex, endIndex);
{/* Item */}
{renderItem(item, index, itemRefs)}
</React.Fragment>
))}
return (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div
style={{
display: 'flex',
gap: GRID_CONSTANTS.ITEM_GAP,
justifyContent: 'flex-start',
height: '100%',
alignItems: 'center',
position: 'relative'
}}
>
{rowItems.map((item, itemIndex) => {
const actualIndex = startIndex + itemIndex;
return (
<React.Fragment key={item.id}>
{/* Item */}
{renderItem(item, actualIndex, itemRefs)}
</React.Fragment>
);
})}
{/* End drop zone */}
<div className="w-[20rem] h-[20rem] flex items-center justify-center flex-shrink-0">
<div
data-drop-zone="end"
className={`cursor-pointer select-none w-[15rem] h-[15rem] flex items-center justify-center flex-shrink-0 shadow-sm hover:shadow-md transition-all relative ${
dropTarget === 'end'
? 'ring-2 ring-green-500 bg-green-50'
: 'bg-white hover:bg-blue-50 border-2 border-dashed border-gray-300 hover:border-blue-400'
}`}
style={{ borderRadius: '12px' }}
onDragOver={onDragOver}
onDragEnter={onEndZoneDragEnter}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, 'end')}
>
<div className="text-gray-500 text-sm text-center font-medium">
Drop here to<br />move to end
</div>
</div>
</div>
</div>
);
})}
</div>
{/* Multi-item drag indicator */}
{multiItemDrag && dragPosition && (
<div
className={styles.multiDragIndicator}
style={{
left: dragPosition.x,
top: dragPosition.y,
}}
>
{multiItemDrag.count} items
</div>
)}
</Box>
);
};
@@ -1,23 +1,25 @@
import React, { useState } from 'react';
import { Text, Checkbox, Tooltip, ActionIcon, Badge, Modal } from '@mantine/core';
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
import { ActionIcon, CheckboxIndicator } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import CloseIcon from '@mui/icons-material/Close';
import VisibilityIcon from '@mui/icons-material/Visibility';
import HistoryIcon from '@mui/icons-material/History';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
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 styles from './PageEditor.module.css';
import FileOperationHistory from '../history/FileOperationHistory';
import { useFileContext } from '../../contexts/FileContext';
import { FileId } from '../../types/file';
interface FileItem {
id: string;
id: FileId;
name: string;
pageCount: number;
thumbnail: string;
thumbnail: string | null;
size: number;
splitBefore?: boolean;
modifiedAt?: number | string | Date;
}
interface FileThumbnailProps {
@@ -26,20 +28,12 @@ interface FileThumbnailProps {
totalFiles: number;
selectedFiles: string[];
selectionMode: boolean;
draggedFile: string | null;
dropTarget: string | null;
isAnimating: boolean;
fileRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
onDragStart: (fileId: string) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent) => void;
onDragEnter: (fileId: string) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, fileId: string) => void;
onToggleFile: (fileId: string) => void;
onDeleteFile: (fileId: string) => void;
onViewFile: (fileId: string) => void;
onToggleFile: (fileId: FileId) => void;
onDeleteFile: (fileId: FileId) => void;
onViewFile: (fileId: FileId) => void;
onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
onDownloadFile?: (fileId: FileId) => void;
toolMode?: boolean;
isSupported?: boolean;
}
@@ -47,122 +41,259 @@ interface FileThumbnailProps {
const FileThumbnail = ({
file,
index,
totalFiles,
selectedFiles,
selectionMode,
draggedFile,
dropTarget,
isAnimating,
fileRefs,
onDragStart,
onDragEnd,
onDragOver,
onDragEnter,
onDragLeave,
onDrop,
onToggleFile,
onDeleteFile,
onViewFile,
onSetStatus,
toolMode = false,
onReorderFiles,
onDownloadFile,
isSupported = true,
}: FileThumbnailProps) => {
const { t } = useTranslation();
const [showHistory, setShowHistory] = useState(false);
const { pinnedFiles, pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext();
const { pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext();
// Find the actual File object that corresponds to this FileItem
const actualFile = activeFiles.find(f => f.name === file.name && f.size === file.size);
// ---- Drag state ----
const [isDragging, setIsDragging] = useState(false);
const dragElementRef = useRef<HTMLDivElement | null>(null);
const [actionsWidth, setActionsWidth] = useState<number | undefined>(undefined);
const [showActions, setShowActions] = useState(false);
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
// Resolve the actual File object for pin/unpin operations
const actualFile = useMemo(() => {
return activeFiles.find(f => f.fileId === file.id);
}, [activeFiles, file.id]);
const isPinned = actualFile ? isFilePinned(actualFile) : false;
const downloadSelectedFile = useCallback(() => {
// Prefer parent-provided handler if available
if (typeof onDownloadFile === 'function') {
onDownloadFile(file.id);
return;
}
// Fallback: attempt to download using the File object if provided
const maybeFile = (file as unknown as { file?: File }).file;
if (maybeFile instanceof File) {
const link = document.createElement('a');
link.href = URL.createObjectURL(maybeFile);
link.download = maybeFile.name || file.name || 'download';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
return;
}
// If we can't find a way to download, surface a status message
onSetStatus?.(typeof t === 'function' ? t('downloadUnavailable', 'Download unavailable for this item') : 'Download unavailable for this item');
}, [file, onDownloadFile, onSetStatus, t]);
const handleRef = useRef<HTMLSpanElement | null>(null);
// ---- Selection ----
const isSelected = selectedFiles.includes(file.id);
// ---- Drag & drop wiring ----
const fileElementRef = useCallback((element: HTMLDivElement | null) => {
if (!element) return;
dragElementRef.current = element;
const dragCleanup = draggable({
element,
getInitialData: () => ({
type: 'file',
fileId: file.id,
fileName: file.name,
selectedFiles: [file.id] // Always drag only this file, ignore selection state
}),
onDragStart: () => {
setIsDragging(true);
},
onDrop: () => {
setIsDragging(false);
}
});
const dropCleanup = dropTargetForElements({
element,
getData: () => ({
type: 'file',
fileId: file.id
}),
canDrop: ({ source }) => {
const sourceData = source.data;
return sourceData.type === 'file' && sourceData.fileId !== file.id;
},
onDrop: ({ source }) => {
const sourceData = source.data;
if (sourceData.type === 'file' && onReorderFiles) {
const sourceFileId = sourceData.fileId as FileId;
const selectedFileIds = sourceData.selectedFiles as FileId[];
onReorderFiles(sourceFileId, file.id, selectedFileIds);
}
}
});
return () => {
dragCleanup();
dropCleanup();
};
}, [file.id, file.name, selectedFiles, onReorderFiles]);
// Update dropdown width on resize
useEffect(() => {
const update = () => {
if (dragElementRef.current) setActionsWidth(dragElementRef.current.offsetWidth);
};
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
// Close the actions dropdown when hovering outside this file card (and its dropdown)
useEffect(() => {
if (!showActions) return;
const isInsideCard = (target: EventTarget | null) => {
const container = dragElementRef.current;
if (!container) return false;
return target instanceof Node && container.contains(target);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isInsideCard(e.target)) {
setShowActions(false);
}
};
const handleTouchStart = (e: TouchEvent) => {
// On touch devices, close if the touch target is outside the card
if (!isInsideCard(e.target)) {
setShowActions(false);
}
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('touchstart', handleTouchStart, { passive: true });
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('touchstart', handleTouchStart);
};
}, [showActions]);
// ---- Card interactions ----
const handleCardClick = () => {
if (!isSupported) return;
onToggleFile(file.id);
};
return (
<div
ref={(el) => {
if (el) {
fileRefs.current.set(file.id, el);
} else {
fileRefs.current.delete(file.id);
}
}}
ref={fileElementRef}
data-file-id={file.id}
data-testid="file-thumbnail"
className={`
${styles.pageContainer}
!rounded-lg
cursor-grab
select-none
w-[20rem]
h-[24rem]
flex flex-col items-center justify-center
flex-shrink-0
shadow-sm
hover:shadow-md
transition-all
relative
${selectionMode
? 'bg-white hover:bg-gray-50'
: 'bg-white hover:bg-gray-50'}
${draggedFile === file.id ? 'opacity-50 scale-95' : ''}
`}
data-selected={isSelected}
data-supported={isSupported}
className={`${styles.card} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative`}
style={{
transform: (() => {
if (!isAnimating && draggedFile && file.id !== draggedFile && dropTarget === file.id) {
return 'translateX(20px)';
}
return 'translateX(0)';
})(),
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
opacity: isSupported ? 1 : 0.5,
filter: isSupported ? 'none' : 'grayscale(50%)'
opacity: isSupported ? (isDragging ? 0.9 : 1) : 0.5,
filter: isSupported ? 'none' : 'grayscale(50%)',
}}
draggable
onDragStart={() => onDragStart(file.id)}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
onDragEnter={() => onDragEnter(file.id)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, file.id)}
tabIndex={0}
role="listitem"
aria-selected={isSelected}
onClick={handleCardClick}
>
{selectionMode && (
<div
className={styles.checkboxContainer}
data-testid="file-thumbnail-checkbox"
style={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 4,
backgroundColor: 'white',
borderRadius: '4px',
padding: '2px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'auto'
}}
onMouseDown={(e) => e.stopPropagation()}
onDragStart={(e) => {
e.preventDefault();
{/* Header bar */}
<div
className={`${styles.header} ${
isSelected ? styles.headerSelected : styles.headerResting
}`}
>
{/* Logo/checkbox area */}
<div className={styles.logoMark}>
{isSupported ? (
<CheckboxIndicator
checked={isSelected}
onChange={() => onToggleFile(file.id)}
color="var(--checkbox-checked-bg)"
/>
) : (
<div className={styles.unsupportedPill}>
<span>
{t('unsupported', 'Unsupported')}
</span>
</div>
)}
</div>
{/* Centered index */}
<div className={styles.headerIndex} aria-label={`Position ${index + 1}`}>
{index + 1}
</div>
{/* Kebab menu */}
<ActionIcon
aria-label={t('moreOptions', 'More options')}
variant="subtle"
className={styles.kebab}
onClick={(e) => {
e.stopPropagation();
setShowActions((v) => !v);
}}
>
<Checkbox
checked={selectedFiles.includes(file.id)}
onChange={(event) => {
event.stopPropagation();
if (isSupported) {
onToggleFile(file.id);
<MoreVertIcon fontSize="small" />
</ActionIcon>
</div>
{/* Actions overlay */}
{showActions && (
<div
className={styles.actionsOverlay}
style={{ width: actionsWidth }}
onClick={(e) => e.stopPropagation()}
>
<button
className={styles.actionRow}
onClick={() => {
if (actualFile) {
if (isPinned) {
unpinFile(actualFile);
onSetStatus?.(`Unpinned ${file.name}`);
} else {
pinFile(actualFile);
onSetStatus?.(`Pinned ${file.name}`);
}
}
setShowActions(false);
}}
onClick={(e) => e.stopPropagation()}
disabled={!isSupported}
size="sm"
/>
>
{isPinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
<span>{isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}</span>
</button>
<button
className={styles.actionRow}
onClick={() => { downloadSelectedFile(); setShowActions(false); }}
>
<DownloadOutlinedIcon fontSize="small" />
<span>{t('download', 'Download')}</span>
</button>
<div className={styles.actionsDivider} />
<button
className={`${styles.actionRow} ${styles.actionDanger}`}
onClick={() => {
onDeleteFile(file.id);
onSetStatus(`Deleted ${file.name}`);
setShowActions(false);
}}
>
<DeleteOutlineIcon fontSize="small" />
<span>{t('delete', 'Delete')}</span>
</button>
</div>
)}
@@ -184,210 +315,46 @@ const FileThumbnail = ({
boxShadow: '2px 2px 0 rgba(0,0,0,0.1), 4px 4px 0 rgba(0,0,0,0.05)'
}}
>
<img
src={file.thumbnail}
alt={file.name}
{file.thumbnail && (
<img
src={file.thumbnail}
alt={file.name}
draggable={false}
onError={(e) => {
// Hide broken image if blob URL was revoked
const img = e.target as HTMLImageElement;
img.style.display = 'none';
}}
style={{
maxWidth: '100%',
maxHeight: '100%',
maxWidth: '80%',
maxHeight: '80%',
objectFit: 'contain',
borderRadius: 2,
borderRadius: 0,
background: '#ffffff',
border: '1px solid var(--border-default)',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
alignSelf: 'start'
}}
/>
)}
</div>
{/* Page count badge */}
<Badge
size="sm"
variant="filled"
color="blue"
style={{
position: 'absolute',
top: 8,
left: 8,
zIndex: 3,
}}
>
{file.pageCount} pages
</Badge>
{/* Unsupported badge */}
{!isSupported && (
<Badge
size="sm"
variant="filled"
color="orange"
style={{
position: 'absolute',
top: 8,
right: selectionMode ? 48 : 8, // Avoid overlap with checkbox
zIndex: 3,
}}
>
{t("fileManager.unsupported", "Unsupported")}
</Badge>
{/* Pin indicator (bottom-left) */}
{isPinned && (
<span className={styles.pinIndicator} aria-hidden>
<PushPinIcon fontSize="small" />
</span>
)}
{/* File name overlay */}
<Text
className={styles.pageNumber}
size="xs"
fw={500}
c="white"
style={{
position: 'absolute',
bottom: 5,
left: 5,
right: 5,
background: 'rgba(0, 0, 0, 0.8)',
padding: '4px 6px',
borderRadius: 4,
zIndex: 2,
opacity: 0,
transition: 'opacity 0.2s ease-in-out',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap'
}}
>
{file.name}
</Text>
{/* Hover controls */}
<div
className={styles.pageHoverControls}
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'rgba(0, 0, 0, 0.8)',
padding: '8px 12px',
borderRadius: 20,
opacity: 0,
transition: 'opacity 0.2s ease-in-out',
zIndex: 3,
display: 'flex',
gap: '8px',
alignItems: 'center',
whiteSpace: 'nowrap'
}}
>
{!toolMode && isSupported && (
<>
<Tooltip label="View File">
<ActionIcon
size="md"
variant="subtle"
c="white"
onClick={(e) => {
e.stopPropagation();
onViewFile(file.id);
onSetStatus(`Opened ${file.name}`);
}}
>
<VisibilityIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
</>
)}
<Tooltip label="View History">
<ActionIcon
size="md"
variant="subtle"
c="white"
onClick={(e) => {
e.stopPropagation();
setShowHistory(true);
onSetStatus(`Viewing history for ${file.name}`);
}}
>
<HistoryIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
{actualFile && (
<Tooltip label={isFilePinned(actualFile) ? "Unpin File" : "Pin File"}>
<ActionIcon
size="md"
variant="subtle"
c={isFilePinned(actualFile) ? "yellow" : "white"}
onClick={(e) => {
e.stopPropagation();
if (isFilePinned(actualFile)) {
unpinFile(actualFile);
onSetStatus(`Unpinned ${file.name}`);
} else {
pinFile(actualFile);
onSetStatus(`Pinned ${file.name}`);
}
}}
>
{isFilePinned(actualFile) ? (
<PushPinIcon style={{ fontSize: 20 }} />
) : (
<PushPinOutlinedIcon style={{ fontSize: 20 }} />
)}
</ActionIcon>
</Tooltip>
)}
<Tooltip label="Close File">
<ActionIcon
size="md"
variant="subtle"
c="orange"
onClick={(e) => {
e.stopPropagation();
onDeleteFile(file.id);
onSetStatus(`Closed ${file.name}`);
}}
>
<CloseIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
</div>
<DragIndicatorIcon
style={{
position: 'absolute',
bottom: 4,
right: 4,
color: 'rgba(0,0,0,0.3)',
fontSize: 16,
zIndex: 1
}}
/>
{/* Drag handle (span wrapper so we can attach a ref reliably) */}
<span ref={handleRef} className={styles.dragHandle} aria-hidden>
<DragIndicatorIcon fontSize="small" />
</span>
</div>
{/* File info */}
<div className="w-full px-4 py-2 text-center">
<Text size="sm" fw={500} truncate>
{file.name}
</Text>
<Text size="xs" c="dimmed">
{formatFileSize(file.size)}
</Text>
</div>
{/* History Modal */}
<Modal
opened={showHistory}
onClose={() => setShowHistory(false)}
title={`Operation History - ${file.name}`}
size="lg"
scrollAreaComponent={'div' as any}
>
<FileOperationHistory
fileId={file.name}
showOnlyApplied={true}
maxHeight={500}
/>
</Modal>
</div>
);
};
export default FileThumbnail;
export default React.memo(FileThumbnail);
@@ -16,7 +16,7 @@
}
.pageContainer:hover .pageHoverControls {
opacity: 1 !important;
opacity: 0.95 !important;
}
/* Checkbox container - prevent transform inheritance */
@@ -62,6 +62,40 @@
}
}
.pulse {
animation: pulse 1s infinite;
}
/* Action styles */
.actionRow:hover {
background: var(--hover-bg);
}
.actionDanger {
color: var(--text-brand-accent);
}
.actionsDivider {
height: 1px;
background: var(--border-default);
margin: 4px 0;
}
.pinIndicator {
position: absolute;
bottom: 4px;
left: 4px;
z-index: 1;
color: rgba(0, 0, 0, 0.35); /* match drag handle color */
}
.unsupportedPill {
margin-left: 1.75rem;
background: #6B7280;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
min-width: 80px;
height: 20px;
}
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,14 @@
import React from "react";
import {
Tooltip,
ActionIcon,
Paper
} from "@mantine/core";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import ContentCutIcon from "@mui/icons-material/ContentCut";
import DownloadIcon from "@mui/icons-material/Download";
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import RotateRightIcon from "@mui/icons-material/RotateRight";
import DeleteIcon from "@mui/icons-material/Delete";
import CloseIcon from "@mui/icons-material/Close";
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
interface PageEditorControlsProps {
// Close/Reset functions
@@ -27,19 +24,25 @@ interface PageEditorControlsProps {
onRotate: (direction: 'left' | 'right') => void;
onDelete: () => void;
onSplit: () => void;
onSplitAll: () => void;
onPageBreak: () => void;
onPageBreakAll: () => void;
// Export functions
onExportSelected: () => void;
// Export functions (moved to right rail)
onExportAll: () => void;
exportLoading: boolean;
// Selection state
selectionMode: boolean;
selectedPages: number[];
selectedPageIds: string[];
displayDocument?: { pages: { id: string; pageNumber: number }[] };
// Split state (for tooltip logic)
splitPositions?: Set<number>;
totalPages?: number;
}
const PageEditorControls = ({
onClosePdf,
onUndo,
onRedo,
canUndo,
@@ -47,19 +50,62 @@ const PageEditorControls = ({
onRotate,
onDelete,
onSplit,
onExportSelected,
onExportAll,
exportLoading,
selectionMode,
selectedPages
onPageBreak,
selectedPageIds,
displayDocument,
splitPositions,
totalPages
}: PageEditorControlsProps) => {
// Calculate split tooltip text using smart toggle logic
const getSplitTooltip = () => {
if (!splitPositions || !totalPages || selectedPageIds.length === 0) {
return "Split Selected";
}
// Convert selected pages to split positions (same logic as handleSplit)
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
const page = displayDocument.pages.find(p => p.id === id);
return page?.pageNumber || 0;
}).filter(num => num > 0) : [];
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
if (selectedSplitPositions.length === 0) {
return "Split Selected";
}
// Smart toggle logic: follow the majority, default to adding splits if equal
const existingSplitsCount = selectedSplitPositions.filter(pos => splitPositions.has(pos)).length;
const noSplitsCount = selectedSplitPositions.length - existingSplitsCount;
// Remove splits only if majority already have splits
// If equal (50/50), default to adding splits
const willRemoveSplits = existingSplitsCount > noSplitsCount;
if (willRemoveSplits) {
return existingSplitsCount === selectedSplitPositions.length
? "Remove All Selected Splits"
: "Remove Selected Splits";
} else {
return existingSplitsCount === 0
? "Split Selected"
: "Complete Selected Splits";
}
};
// Calculate page break tooltip text
const getPageBreakTooltip = () => {
return selectedPageIds.length > 0
? `Insert ${selectedPageIds.length} Page Break${selectedPageIds.length > 1 ? 's' : ''}`
: "Insert Page Breaks";
};
return (
<div
style={{
position: 'absolute',
left: '50%',
bottom: '20px',
transform: 'translateX(-50%)',
left: 0,
right: 0,
bottom: 0,
zIndex: 50,
display: 'flex',
justifyContent: 'center',
@@ -67,43 +113,37 @@ const PageEditorControls = ({
background: 'transparent',
}}
>
<Paper
radius="xl"
shadow="lg"
p={16}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
borderRadius: 32,
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
boxShadow: '0 -2px 8px rgba(0,0,0,0.04)',
backgroundColor: 'var(--bg-toolbar)',
border: '1px solid var(--border-default)',
borderRadius: '16px 16px 0 0',
pointerEvents: 'auto',
minWidth: 400,
justifyContent: 'center'
minWidth: 360,
maxWidth: 700,
flexWrap: 'wrap',
justifyContent: 'center',
padding: "1rem",
paddingBottom: "1rem"
}}
>
{/* Close PDF */}
<Tooltip label="Close PDF">
<ActionIcon
onClick={onClosePdf}
color="red"
variant="light"
size="lg"
>
<CloseIcon />
</ActionIcon>
</Tooltip>
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
{/* Undo/Redo */}
<Tooltip label="Undo">
<ActionIcon onClick={onUndo} disabled={!canUndo} size="lg">
<ActionIcon onClick={onUndo} disabled={!canUndo} variant="subtle" radius="md" size="lg">
<UndoIcon />
</ActionIcon>
</Tooltip>
<Tooltip label="Redo">
<ActionIcon onClick={onRedo} disabled={!canRedo} size="lg">
<ActionIcon onClick={onRedo} disabled={!canRedo} variant="subtle" radius="md" size="lg">
<RedoIcon />
</ActionIcon>
</Tooltip>
@@ -111,79 +151,67 @@ const PageEditorControls = ({
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
{/* Page Operations */}
<Tooltip label={selectionMode ? "Rotate Selected Left" : "Rotate All Left"}>
<Tooltip label="Rotate Selected Left">
<ActionIcon
onClick={() => onRotate('left')}
disabled={selectionMode && selectedPages.length === 0}
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
radius="md"
size="lg"
>
<RotateLeftIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={selectionMode ? "Rotate Selected Right" : "Rotate All Right"}>
<Tooltip label="Rotate Selected Right">
<ActionIcon
onClick={() => onRotate('right')}
disabled={selectionMode && selectedPages.length === 0}
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
radius="md"
size="lg"
>
<RotateRightIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={selectionMode ? "Delete Selected" : "Delete All"}>
<Tooltip label="Delete Selected">
<ActionIcon
onClick={onDelete}
disabled={selectionMode && selectedPages.length === 0}
color="red"
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
radius="md"
size="lg"
>
<DeleteIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={selectionMode ? "Split Selected" : "Split All"}>
<Tooltip label={getSplitTooltip()}>
<ActionIcon
onClick={onSplit}
disabled={selectionMode && selectedPages.length === 0}
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
color={selectionMode && selectedPages.length > 0 ? "blue" : undefined}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
radius="md"
size="lg"
>
<ContentCutIcon />
</ActionIcon>
</Tooltip>
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
{/* Export Controls */}
{selectionMode && selectedPages.length > 0 && (
<Tooltip label="Export Selected">
<ActionIcon
onClick={onExportSelected}
disabled={exportLoading}
color="blue"
variant="light"
size="lg"
>
<DownloadIcon />
</ActionIcon>
</Tooltip>
)}
<Tooltip label="Export All">
<Tooltip label={getPageBreakTooltip()}>
<ActionIcon
onClick={onExportAll}
disabled={exportLoading}
color="green"
variant="light"
onClick={onPageBreak}
disabled={selectedPageIds.length === 0}
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
radius="md"
size="lg"
>
<DownloadIcon />
<InsertPageBreakIcon />
</ActionIcon>
</Tooltip>
</Paper>
</div>
</div>
);
};
@@ -1,140 +1,286 @@
import React, { useCallback, useState, useEffect, useRef } from 'react';
import { Text, Checkbox, Tooltip, ActionIcon, Loader } from '@mantine/core';
import { Text, Checkbox, Tooltip, ActionIcon } from '@mantine/core';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import RotateLeftIcon from '@mui/icons-material/RotateLeft';
import RotateRightIcon from '@mui/icons-material/RotateRight';
import DeleteIcon from '@mui/icons-material/Delete';
import ContentCutIcon from '@mui/icons-material/ContentCut';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import AddIcon from '@mui/icons-material/Add';
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { PDFPage, PDFDocument } from '../../types/pageEditor';
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../commands/pageCommands';
import { Command } from '../../hooks/useUndoRedo';
import { useThumbnailGeneration } from '../../hooks/useThumbnailGeneration';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import styles from './PageEditor.module.css';
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
// Ensure PDF.js worker is available
if (!GlobalWorkerOptions.workerSrc) {
GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
console.log('📸 PageThumbnail: Set PDF.js worker source to /pdf.worker.js');
} else {
console.log('📸 PageThumbnail: PDF.js worker source already set to', GlobalWorkerOptions.workerSrc);
}
interface PageThumbnailProps {
page: PDFPage;
index: number;
totalPages: number;
originalFile?: File; // For lazy thumbnail generation
selectedPages: number[];
originalFile?: File;
selectedPageIds: string[];
selectionMode: boolean;
draggedPage: number | null;
dropTarget: number | 'end' | null;
movingPage: number | null;
isAnimating: boolean;
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
onDragStart: (pageNumber: number) => void;
onDragEnd: () => void;
onDragOver: (e: React.DragEvent) => void;
onDragEnter: (pageNumber: number) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, pageNumber: number) => void;
onTogglePage: (pageNumber: number) => void;
onAnimateReorder: (pageNumber: number, targetIndex: number) => void;
onExecuteCommand: (command: Command) => void;
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
onTogglePage: (pageId: string) => void;
onAnimateReorder: () => void;
onExecuteCommand: (command: { execute: () => void }) => void;
onSetStatus: (status: string) => void;
onSetMovingPage: (pageNumber: number | null) => void;
RotatePagesCommand: typeof RotatePagesCommand;
DeletePagesCommand: typeof DeletePagesCommand;
ToggleSplitCommand: typeof ToggleSplitCommand;
onSetMovingPage: (page: number | null) => void;
onDeletePage: (pageNumber: number) => void;
createRotateCommand: (pageIds: string[], rotation: number) => { execute: () => void };
createDeleteCommand: (pageIds: string[]) => { execute: () => void };
createSplitCommand: (position: number) => { execute: () => void };
pdfDocument: PDFDocument;
setPdfDocument: (doc: PDFDocument) => void;
splitPositions: Set<number>;
onInsertFiles?: (files: File[], insertAfterPage: number) => void;
}
const PageThumbnail = React.memo(({
const PageThumbnail: React.FC<PageThumbnailProps> = ({
page,
index,
totalPages,
originalFile,
selectedPages,
selectedPageIds,
selectionMode,
draggedPage,
dropTarget,
movingPage,
isAnimating,
pageRefs,
onDragStart,
onDragEnd,
onDragOver,
onDragEnter,
onDragLeave,
onDrop,
onReorderPages,
onTogglePage,
onAnimateReorder,
onExecuteCommand,
onSetStatus,
onSetMovingPage,
RotatePagesCommand,
DeletePagesCommand,
ToggleSplitCommand,
onDeletePage,
createRotateCommand,
createSplitCommand,
pdfDocument,
setPdfDocument,
splitPositions,
onInsertFiles,
}: PageThumbnailProps) => {
const [isDragging, setIsDragging] = useState(false);
const [isMouseDown, setIsMouseDown] = useState(false);
const [mouseStartPos, setMouseStartPos] = useState<{x: number, y: number} | null>(null);
const dragElementRef = useRef<HTMLDivElement>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
const [isLoadingThumbnail, setIsLoadingThumbnail] = useState(false);
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
const { openFilesModal } = useFilesModalContext();
// Calculate document aspect ratio from first non-blank page
const getDocumentAspectRatio = useCallback(() => {
// Find first non-blank page with a thumbnail to get aspect ratio
const firstRealPage = pdfDocument.pages.find(p => !p.isBlankPage && p.thumbnail);
if (firstRealPage?.thumbnail) {
// Try to get aspect ratio from an actual thumbnail image
// For now, default to A4 but could be enhanced to measure image dimensions
return '1 / 1.414'; // A4 ratio as fallback
}
return '1 / 1.414'; // Default A4 ratio
}, [pdfDocument.pages]);
// Update thumbnail URL when page prop changes
useEffect(() => {
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
console.log(`📸 PageThumbnail: Updating thumbnail URL for page ${page.pageNumber}`, page.thumbnail.substring(0, 50) + '...');
setThumbnailUrl(page.thumbnail);
}
}, [page.thumbnail, page.pageNumber, page.id, thumbnailUrl]);
}, [page.thumbnail, thumbnailUrl]);
// Listen for ready thumbnails from Web Workers (only if no existing thumbnail)
// Request thumbnail if missing (on-demand, virtualized approach)
useEffect(() => {
if (thumbnailUrl) {
console.log(`📸 PageThumbnail: Page ${page.pageNumber} already has thumbnail, skipping worker listener`);
return; // Skip if we already have a thumbnail
let isCancelled = false;
// If we already have a thumbnail, use it
if (page.thumbnail) {
setThumbnailUrl(page.thumbnail);
return;
}
console.log(`📸 PageThumbnail: Setting up worker listener for page ${page.pageNumber} (${page.id})`);
// Check cache first
const cachedThumbnail = getThumbnailFromCache(page.id);
if (cachedThumbnail) {
setThumbnailUrl(cachedThumbnail);
return;
}
const handleThumbnailReady = (event: CustomEvent) => {
const { pageNumber, thumbnail, pageId } = event.detail;
console.log(`📸 PageThumbnail: Received worker thumbnail for page ${pageNumber}, looking for page ${page.pageNumber} (${page.id})`);
// Request thumbnail generation if we have the original file
if (originalFile) {
const pageNumber = page.originalPageNumber;
if (pageNumber === page.pageNumber && pageId === page.id) {
console.log(`✓ PageThumbnail: Thumbnail matched for page ${page.pageNumber}, setting URL`);
setThumbnailUrl(thumbnail);
}
};
requestThumbnail(page.id, originalFile, pageNumber)
.then(thumbnail => {
if (!isCancelled && thumbnail) {
setThumbnailUrl(thumbnail);
}
})
.catch(error => {
console.warn(`Failed to generate thumbnail for ${page.id}:`, error);
});
}
window.addEventListener('thumbnailReady', handleThumbnailReady as EventListener);
return () => {
console.log(`📸 PageThumbnail: Cleaning up worker listener for page ${page.pageNumber}`);
window.removeEventListener('thumbnailReady', handleThumbnailReady as EventListener);
isCancelled = true;
};
}, [page.pageNumber, page.id, thumbnailUrl]);
}, [page.id, page.thumbnail, originalFile, getThumbnailFromCache, requestThumbnail]);
// Register this component with pageRefs for animations
const pageElementRef = useCallback((element: HTMLDivElement | null) => {
if (element) {
pageRefs.current.set(page.id, element);
dragElementRef.current = element;
const dragCleanup = draggable({
element,
getInitialData: () => ({
pageNumber: page.pageNumber,
pageId: page.id,
selectedPageIds: [page.id]
}),
onDragStart: () => {
setIsDragging(true);
},
onDrop: ({ location }) => {
setIsDragging(false);
if (location.current.dropTargets.length === 0) {
return;
}
const dropTarget = location.current.dropTargets[0];
const targetData = dropTarget.data;
if (targetData.type === 'page') {
const targetPageNumber = targetData.pageNumber as number;
const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber);
if (targetIndex !== -1) {
onReorderPages(page.pageNumber, targetIndex, undefined);
}
}
}
});
element.style.cursor = 'grab';
const dropCleanup = dropTargetForElements({
element,
getData: () => ({
type: 'page',
pageNumber: page.pageNumber
}),
onDrop: (_) => {}
});
(element as any).__dragCleanup = () => {
dragCleanup();
dropCleanup();
};
} else {
pageRefs.current.delete(page.id);
if (dragElementRef.current && (dragElementRef.current as any).__dragCleanup) {
(dragElementRef.current as any).__dragCleanup();
}
}
}, [page.id, pageRefs]);
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]);
// DOM command handlers
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
// Use the command system for undo/redo support
const command = createRotateCommand([page.id], -90);
onExecuteCommand(command);
onSetStatus(`Rotated page ${page.pageNumber} left`);
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, createRotateCommand]);
const handleRotateRight = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
// Use the command system for undo/redo support
const command = createRotateCommand([page.id], 90);
onExecuteCommand(command);
onSetStatus(`Rotated page ${page.pageNumber} right`);
}, [page.id, page.pageNumber, onExecuteCommand, onSetStatus, createRotateCommand]);
const handleDelete = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
onDeletePage(page.pageNumber);
onSetStatus(`Deleted page ${page.pageNumber}`);
}, [page.pageNumber, onDeletePage, onSetStatus]);
const handleSplit = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
// Create a command to toggle split at this position
const command = createSplitCommand(index);
onExecuteCommand(command);
const hasSplit = splitPositions.has(index);
const action = hasSplit ? 'removed' : 'added';
onSetStatus(`Split marker ${action} after position ${index + 1}`);
}, [index, splitPositions, onExecuteCommand, onSetStatus, createSplitCommand]);
const handleInsertFileAfter = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (onInsertFiles) {
// Open file manager modal with custom handler for page insertion
openFilesModal({
insertAfterPage: page.pageNumber,
customHandler: (files: File[], insertAfterPage?: number) => {
if (insertAfterPage !== undefined) {
onInsertFiles(files, insertAfterPage);
}
}
});
onSetStatus(`Select files to insert after page ${page.pageNumber}`);
} else {
// Fallback to normal file handling
openFilesModal({ insertAfterPage: page.pageNumber });
onSetStatus(`Select files to insert after page ${page.pageNumber}`);
}
}, [openFilesModal, page.pageNumber, onSetStatus, onInsertFiles]);
// Handle click vs drag differentiation
const handleMouseDown = useCallback((e: React.MouseEvent) => {
setIsMouseDown(true);
setMouseStartPos({ x: e.clientX, y: e.clientY });
}, []);
const handleMouseUp = useCallback((e: React.MouseEvent) => {
if (!isMouseDown || !mouseStartPos) {
setIsMouseDown(false);
setMouseStartPos(null);
return;
}
// Calculate distance moved
const deltaX = Math.abs(e.clientX - mouseStartPos.x);
const deltaY = Math.abs(e.clientY - mouseStartPos.y);
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// If mouse moved less than 5 pixels, consider it a click (not a drag)
if (distance < 5 && !isDragging) {
onTogglePage(page.id);
}
setIsMouseDown(false);
setMouseStartPos(null);
}, [isMouseDown, mouseStartPos, isDragging, page.id, onTogglePage]);
const handleMouseLeave = useCallback(() => {
setIsMouseDown(false);
setMouseStartPos(null);
}, []);
return (
<div
ref={pageElementRef}
data-page-id={page.id}
data-page-number={page.pageNumber}
className={`
${styles.pageContainer}
!rounded-lg
cursor-grab
${selectionMode ? 'cursor-pointer' : 'cursor-grab'}
select-none
w-[20rem]
h-[20rem]
@@ -147,27 +293,18 @@ const PageThumbnail = React.memo(({
${selectionMode
? 'bg-white hover:bg-gray-50'
: 'bg-white hover:bg-gray-50'}
${draggedPage === page.pageNumber ? 'opacity-50 scale-95' : ''}
${isDragging ? 'opacity-50 scale-95' : ''}
${movingPage === page.pageNumber ? 'page-moving' : ''}
`}
style={{
transform: (() => {
if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) {
return 'translateX(20px)';
}
return 'translateX(0)';
})(),
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
}}
draggable
onDragStart={() => onDragStart(page.pageNumber)}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
onDragEnter={() => onDragEnter(page.pageNumber)}
onDragLeave={onDragLeave}
onDrop={(e) => onDrop(e, page.pageNumber)}
draggable={false}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
>
{selectionMode && (
{
<div
className={styles.checkboxContainer}
style={{
@@ -175,36 +312,34 @@ const PageThumbnail = React.memo(({
top: 8,
right: 8,
zIndex: 10,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: '1px solid #ccc',
backgroundColor: 'white',
borderRadius: '4px',
padding: '4px',
padding: '2px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'auto',
cursor: 'pointer'
pointerEvents: 'auto'
}}
onMouseDown={(e) => e.stopPropagation()}
onMouseDown={(e) => {
e.stopPropagation();
onTogglePage(page.id);
}}
onMouseUp={(e) => e.stopPropagation()}
onDragStart={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
console.log('📸 Checkbox clicked for page', page.pageNumber);
e.stopPropagation();
onTogglePage(page.pageNumber);
}}
>
<Checkbox
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
checked={Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false}
onChange={() => {
// onChange is handled by the parent div click
// Selection is handled by container mouseDown
}}
size="sm"
style={{ pointerEvents: 'none' }}
/>
</div>
)}
}
<div className="page-container w-[90%] h-[90%]">
<div className="page-container w-[90%] h-[90%]" draggable={false}>
<div
style={{
width: '100%',
@@ -218,10 +353,27 @@ const PageThumbnail = React.memo(({
justifyContent: 'center'
}}
>
{thumbnailUrl ? (
{page.isBlankPage ? (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
width: '70%',
aspectRatio: getDocumentAspectRatio(),
backgroundColor: 'white',
border: '1px solid #e9ecef',
borderRadius: 2
}}></div>
</div>
) : thumbnailUrl ? (
<img
src={thumbnailUrl}
alt={`Page ${page.pageNumber}`}
draggable={false}
style={{
width: '100%',
height: '100%',
@@ -231,11 +383,6 @@ const PageThumbnail = React.memo(({
transition: 'transform 0.3s ease-in-out'
}}
/>
) : isLoadingThumbnail ? (
<div style={{ textAlign: 'center' }}>
<Loader size="sm" />
<Text size="xs" c="dimmed" mt={4}>Loading...</Text>
</div>
) : (
<div style={{ textAlign: 'center' }}>
<Text size="lg" c="dimmed">📄</Text>
@@ -248,12 +395,12 @@ const PageThumbnail = React.memo(({
className={styles.pageNumber}
size="sm"
fw={500}
c="white"
style={{
color: 'var(--mantine-color-white)', // Use theme token for consistency
position: 'absolute',
top: 5,
left: 5,
background: 'rgba(162, 201, 255, 0.8)',
background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)',
padding: '6px 8px',
borderRadius: 8,
zIndex: 2,
@@ -271,7 +418,8 @@ const PageThumbnail = React.memo(({
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(0, 0, 0, 0.8)',
backgroundColor: 'var(--bg-toolbar)',
border: '1px solid var(--border-default)',
padding: '6px 12px',
borderRadius: 20,
opacity: 0,
@@ -282,19 +430,23 @@ const PageThumbnail = React.memo(({
alignItems: 'center',
whiteSpace: 'nowrap'
}}
onMouseDown={(e) => e.stopPropagation()}
onMouseUp={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<Tooltip label="Move Left">
<ActionIcon
size="md"
variant="subtle"
c="white"
style={{ color: 'var(--mantine-color-dimmed)' }}
disabled={index === 0}
onClick={(e) => {
e.stopPropagation();
if (index > 0 && !movingPage && !isAnimating) {
onSetMovingPage(page.pageNumber);
onAnimateReorder(page.pageNumber, index - 1);
setTimeout(() => onSetMovingPage(null), 500);
// Actually move the page left (swap with previous page)
onReorderPages(page.pageNumber, index - 1);
setTimeout(() => onSetMovingPage(null), 650);
onSetStatus(`Moved page ${page.pageNumber} left`);
}
}}
@@ -307,14 +459,15 @@ const PageThumbnail = React.memo(({
<ActionIcon
size="md"
variant="subtle"
c="white"
style={{ color: 'var(--mantine-color-dimmed)' }}
disabled={index === totalPages - 1}
onClick={(e) => {
e.stopPropagation();
if (index < totalPages - 1 && !movingPage && !isAnimating) {
onSetMovingPage(page.pageNumber);
onAnimateReorder(page.pageNumber, index + 1);
setTimeout(() => onSetMovingPage(null), 500);
// Actually move the page right (swap with next page)
onReorderPages(page.pageNumber, index + 1);
setTimeout(() => onSetMovingPage(null), 650);
onSetStatus(`Moved page ${page.pageNumber} right`);
}
}}
@@ -327,18 +480,8 @@ const PageThumbnail = React.memo(({
<ActionIcon
size="md"
variant="subtle"
c="white"
onClick={(e) => {
e.stopPropagation();
const command = new RotatePagesCommand(
pdfDocument,
setPdfDocument,
[page.id],
-90
);
onExecuteCommand(command);
onSetStatus(`Rotated page ${page.pageNumber} left`);
}}
style={{ color: 'var(--mantine-color-dimmed)' }}
onClick={handleRotateLeft}
>
<RotateLeftIcon style={{ fontSize: 20 }} />
</ActionIcon>
@@ -348,18 +491,8 @@ const PageThumbnail = React.memo(({
<ActionIcon
size="md"
variant="subtle"
c="white"
onClick={(e) => {
e.stopPropagation();
const command = new RotatePagesCommand(
pdfDocument,
setPdfDocument,
[page.id],
90
);
onExecuteCommand(command);
onSetStatus(`Rotated page ${page.pageNumber} right`);
}}
style={{ color: 'var(--mantine-color-dimmed)' }}
onClick={handleRotateRight}
>
<RotateRightIcon style={{ fontSize: 20 }} />
</ActionIcon>
@@ -370,71 +503,41 @@ const PageThumbnail = React.memo(({
size="md"
variant="subtle"
c="red"
onClick={(e) => {
e.stopPropagation();
const command = new DeletePagesCommand(
pdfDocument,
setPdfDocument,
[page.id]
);
onExecuteCommand(command);
onSetStatus(`Deleted page ${page.pageNumber}`);
}}
onClick={handleDelete}
>
<DeleteIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
{index > 0 && (
<Tooltip label="Split Here">
{index < totalPages - 1 && (
<Tooltip label="Split After">
<ActionIcon
size="md"
variant="subtle"
c="white"
onClick={(e) => {
e.stopPropagation();
const command = new ToggleSplitCommand(
pdfDocument,
setPdfDocument,
[page.id]
);
onExecuteCommand(command);
onSetStatus(`Split marker toggled for page ${page.pageNumber}`);
}}
style={{ color: 'var(--mantine-color-dimmed)' }}
onClick={handleSplit}
>
<ContentCutIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
)}
<Tooltip label="Insert File After">
<ActionIcon
size="md"
variant="subtle"
style={{ color: 'var(--mantine-color-dimmed)' }}
onClick={handleInsertFileAfter}
>
<AddIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
</div>
<DragIndicatorIcon
style={{
position: 'absolute',
bottom: 4,
right: 4,
color: 'rgba(0,0,0,0.3)',
fontSize: 16,
zIndex: 1
}}
/>
</div>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if essential props change
return (
prevProps.page.id === nextProps.page.id &&
prevProps.page.pageNumber === nextProps.page.pageNumber &&
prevProps.page.rotation === nextProps.page.rotation &&
prevProps.page.thumbnail === nextProps.page.thumbnail &&
prevProps.selectedPages === nextProps.selectedPages && // Compare array reference - will re-render when selection changes
prevProps.selectionMode === nextProps.selectionMode &&
prevProps.draggedPage === nextProps.draggedPage &&
prevProps.dropTarget === nextProps.dropTarget &&
prevProps.movingPage === nextProps.movingPage &&
prevProps.isAnimating === nextProps.isAnimating
);
});
};
export default PageThumbnail;
@@ -0,0 +1,880 @@
import { FileId } from '../../../types/file';
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
// V1-style DOM-first command system (replaces the old React state commands)
export abstract class DOMCommand {
abstract execute(): void;
abstract undo(): void;
abstract description: string;
}
export class RotatePageCommand extends DOMCommand {
constructor(
private pageId: string,
private degrees: number
) {
super();
}
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;
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;
img.style.transform = `rotate(${previousRotation}deg)`;
}
}
}
get description(): string {
return `Rotate page ${this.degrees > 0 ? 'right' : 'left'}`;
}
}
export class DeletePagesCommand extends DOMCommand {
private originalDocument: PDFDocument | null = null;
private originalSplitPositions: Set<number> = new Set();
private originalSelectedPages: number[] = [];
private hasExecuted: boolean = false;
private pageIdsToDelete: string[] = [];
constructor(
private pagesToDelete: number[],
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void,
private getSplitPositions: () => Set<number>,
private setSplitPositions: (positions: Set<number>) => void,
private getSelectedPages: () => number[]
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc || this.pagesToDelete.length === 0) return;
// Store complete original state for undo (only on first execution)
if (!this.hasExecuted) {
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page})) // Deep copy pages
};
this.originalSplitPositions = new Set(this.getSplitPositions());
this.originalSelectedPages = [...this.getSelectedPages()];
// Convert page numbers to page IDs for stable identification
this.pageIdsToDelete = this.pagesToDelete.map(pageNum => {
const page = currentDoc.pages.find(p => p.pageNumber === pageNum);
return page?.id || '';
}).filter(id => id);
this.hasExecuted = true;
}
// Filter out deleted pages by ID (stable across undo/redo)
const remainingPages = currentDoc.pages.filter(page =>
!this.pageIdsToDelete.includes(page.id)
);
if (remainingPages.length === 0) return; // Safety check
// Renumber remaining pages
remainingPages.forEach((page, index) => {
page.pageNumber = index + 1;
});
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: remainingPages,
totalPages: remainingPages.length,
};
// Adjust split positions
const currentSplitPositions = this.getSplitPositions();
const newPositions = new Set<number>();
currentSplitPositions.forEach(pos => {
if (pos < remainingPages.length - 1) {
newPositions.add(pos);
}
});
// Apply changes
this.setDocument(updatedDocument);
this.setSelectedPages([]);
this.setSplitPositions(newPositions);
}
undo(): void {
if (!this.originalDocument) return;
// Simply restore the complete original document state
this.setDocument(this.originalDocument);
this.setSplitPositions(this.originalSplitPositions);
this.setSelectedPages(this.originalSelectedPages);
}
get description(): string {
return `Delete ${this.pagesToDelete.length} page(s)`;
}
}
export class ReorderPagesCommand extends DOMCommand {
private originalPages: PDFPage[] = [];
constructor(
private sourcePageNumber: number,
private targetIndex: number,
private selectedPages: number[] | undefined,
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc) return;
// Store original state for undo
this.originalPages = currentDoc.pages.map(page => ({...page}));
// Perform the reorder
const sourceIndex = currentDoc.pages.findIndex(p => p.pageNumber === this.sourcePageNumber);
if (sourceIndex === -1) return;
const newPages = [...currentDoc.pages];
if (this.selectedPages && this.selectedPages.length > 1 && this.selectedPages.includes(this.sourcePageNumber)) {
// Multi-page reorder
const selectedPageObjects = this.selectedPages
.map(pageNum => currentDoc.pages.find(p => p.pageNumber === pageNum))
.filter(page => page !== undefined) as PDFPage[];
const remainingPages = newPages.filter(page => !this.selectedPages!.includes(page.pageNumber));
remainingPages.splice(this.targetIndex, 0, ...selectedPageObjects);
remainingPages.forEach((page, index) => {
page.pageNumber = index + 1;
});
newPages.splice(0, newPages.length, ...remainingPages);
} else {
// Single page reorder
const [movedPage] = newPages.splice(sourceIndex, 1);
newPages.splice(this.targetIndex, 0, movedPage);
newPages.forEach((page, index) => {
page.pageNumber = index + 1;
});
}
const reorderedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(reorderedDocument);
}
undo(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc || this.originalPages.length === 0) return;
// Restore original page order
const restoredDocument: PDFDocument = {
...currentDoc,
pages: this.originalPages,
totalPages: this.originalPages.length,
};
this.setDocument(restoredDocument);
}
get description(): string {
return `Reorder page(s)`;
}
}
export class SplitCommand extends DOMCommand {
private originalSplitPositions: Set<number> = new Set();
constructor(
private position: number,
private getSplitPositions: () => Set<number>,
private setSplitPositions: (positions: Set<number>) => void
) {
super();
}
execute(): void {
// Store original state for undo
this.originalSplitPositions = new Set(this.getSplitPositions());
// Toggle the split position
const currentPositions = this.getSplitPositions();
const newPositions = new Set(currentPositions);
if (newPositions.has(this.position)) {
newPositions.delete(this.position);
} else {
newPositions.add(this.position);
}
this.setSplitPositions(newPositions);
}
undo(): void {
// Restore original split positions
this.setSplitPositions(this.originalSplitPositions);
}
get description(): string {
const currentPositions = this.getSplitPositions();
const willAdd = !currentPositions.has(this.position);
return `${willAdd ? 'Add' : 'Remove'} split at position ${this.position + 1}`;
}
}
export class BulkRotateCommand extends DOMCommand {
private originalRotations: Map<string, number> = new Map();
constructor(
private pageIds: string[],
private degrees: number
) {
super();
}
execute(): void {
this.pageIds.forEach(pageId => {
const pageElement = document.querySelector(`[data-page-id="${pageId}"]`);
if (pageElement) {
const img = pageElement.querySelector('img');
if (img) {
// Store original rotation for undo (only on first execution)
if (!this.originalRotations.has(pageId)) {
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
this.originalRotations.set(pageId, currentRotation);
}
// Apply rotation using transform to trigger CSS animation
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
const newRotation = currentRotation + this.degrees;
img.style.transform = `rotate(${newRotation}deg)`;
}
}
});
}
undo(): void {
this.pageIds.forEach(pageId => {
const pageElement = document.querySelector(`[data-page-id="${pageId}"]`);
if (pageElement) {
const img = pageElement.querySelector('img');
if (img && this.originalRotations.has(pageId)) {
img.style.transform = `rotate(${this.originalRotations.get(pageId)}deg)`;
}
}
});
}
get description(): string {
return `Rotate ${this.pageIds.length} page(s) ${this.degrees > 0 ? 'right' : 'left'}`;
}
}
export class BulkSplitCommand extends DOMCommand {
private originalSplitPositions: Set<number> = new Set();
constructor(
private positions: number[],
private getSplitPositions: () => Set<number>,
private setSplitPositions: (positions: Set<number>) => void
) {
super();
}
execute(): void {
// Store original state for undo (only on first execution)
if (this.originalSplitPositions.size === 0) {
this.originalSplitPositions = new Set(this.getSplitPositions());
}
// Toggle each position
const currentPositions = new Set(this.getSplitPositions());
this.positions.forEach(position => {
if (currentPositions.has(position)) {
currentPositions.delete(position);
} else {
currentPositions.add(position);
}
});
this.setSplitPositions(currentPositions);
}
undo(): void {
// Restore original split positions
this.setSplitPositions(this.originalSplitPositions);
}
get description(): string {
return `Toggle ${this.positions.length} split position(s)`;
}
}
export class SplitAllCommand extends DOMCommand {
private originalSplitPositions: Set<number> = new Set();
private allPossibleSplits: Set<number> = new Set();
constructor(
private totalPages: number,
private getSplitPositions: () => Set<number>,
private setSplitPositions: (positions: Set<number>) => void
) {
super();
// Calculate all possible split positions (between pages, not after last page)
for (let i = 0; i < this.totalPages - 1; i++) {
this.allPossibleSplits.add(i);
}
}
execute(): void {
// Store original state for undo
this.originalSplitPositions = new Set(this.getSplitPositions());
// Check if all splits are already active
const currentSplits = this.getSplitPositions();
const hasAllSplits = Array.from(this.allPossibleSplits).every(pos => currentSplits.has(pos));
if (hasAllSplits) {
// Remove all splits
this.setSplitPositions(new Set());
} else {
// Add all splits
this.setSplitPositions(this.allPossibleSplits);
}
}
undo(): void {
// Restore original split positions
this.setSplitPositions(this.originalSplitPositions);
}
get description(): string {
const currentSplits = this.getSplitPositions();
const hasAllSplits = Array.from(this.allPossibleSplits).every(pos => currentSplits.has(pos));
return hasAllSplits ? 'Remove all splits' : 'Split all pages';
}
}
export class PageBreakCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
constructor(
private selectedPageNumbers: number[],
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc || this.selectedPageNumbers.length === 0) return;
// Store original state for undo
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page}))
};
// Create new pages array with blank pages inserted
const newPages: PDFPage[] = [];
this.insertedPages = [];
let pageNumberCounter = 1;
currentDoc.pages.forEach((page, index) => {
// Add the current page
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
newPages.push(updatedPage);
// If this page is selected for page break insertion, add a blank page after it
if (this.selectedPageNumbers.includes(page.pageNumber)) {
const blankPage: PDFPage = {
id: `blank-${Date.now()}-${index}`,
pageNumber: pageNumberCounter++,
originalPageNumber: -1, // Mark as blank page
thumbnail: null,
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: true // Custom flag for blank pages
};
newPages.push(blankPage);
this.insertedPages.push(blankPage);
}
});
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(updatedDocument);
// No need to maintain selection - page IDs remain stable, so selection persists automatically
}
undo(): void {
if (!this.originalDocument) return;
this.setDocument(this.originalDocument);
}
get description(): string {
return `Insert ${this.selectedPageNumbers.length} page break(s)`;
}
}
export class BulkPageBreakCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
private originalSelectedPages: number[] = [];
constructor(
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void,
private getSelectedPages: () => number[]
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc) return;
// Store original selection to restore later
this.originalSelectedPages = this.getSelectedPages();
// Store original state for undo
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page}))
};
// Create new pages array with blank pages inserted after each page (except the last)
const newPages: PDFPage[] = [];
this.insertedPages = [];
let pageNumberCounter = 1;
currentDoc.pages.forEach((page, index) => {
// Add the current page
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
newPages.push(updatedPage);
// Add blank page after each page except the last one
if (index < currentDoc.pages.length - 1) {
const blankPage: PDFPage = {
id: `blank-${Date.now()}-${index}`,
pageNumber: pageNumberCounter++,
originalPageNumber: -1,
thumbnail: null,
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: true
};
newPages.push(blankPage);
this.insertedPages.push(blankPage);
}
});
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(updatedDocument);
// Maintain existing selection by mapping original selected pages to their new positions
const updatedSelection: number[] = [];
this.originalSelectedPages.forEach(originalPageNum => {
// Find the original page by matching the page ID from the original document
const originalPage = this.originalDocument?.pages[originalPageNum - 1];
if (originalPage) {
const foundPage = newPages.find(page => page.id === originalPage.id && !page.isBlankPage);
if (foundPage) {
updatedSelection.push(foundPage.pageNumber);
}
}
});
this.setSelectedPages(updatedSelection);
}
undo(): void {
if (!this.originalDocument) return;
this.setDocument(this.originalDocument);
}
get description(): string {
return `Insert page breaks after all pages`;
}
}
export class InsertFilesCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
private fileDataMap = new Map<FileId, ArrayBuffer>(); // Store file data for thumbnail generation
private originalProcessedFile: any = null; // Store original ProcessedFile for undo
private insertedFileMap = new Map<FileId, File>(); // Store inserted files for export
constructor(
private files: File[],
private insertAfterPageNumber: number,
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void,
private getSelectedPages: () => number[],
private updateFileContext?: (updatedDocument: PDFDocument, insertedFiles?: Map<FileId, File>) => void
) {
super();
}
async execute(): Promise<void> {
const currentDoc = this.getCurrentDocument();
if (!currentDoc || this.files.length === 0) return;
// Store original state for undo
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page}))
};
try {
// Process each file to extract pages and wait for all to complete
const allNewPages: PDFPage[] = [];
// Process all files and wait for their completion
const baseTimestamp = Date.now();
const extractionPromises = this.files.map(async (file, index) => {
const fileId = `inserted-${file.name}-${baseTimestamp + index}` as FileId;
// Store inserted file for export
this.insertedFileMap.set(fileId, file);
// Use base timestamp + index to ensure unique but predictable file IDs
return await this.extractPagesFromFile(file, baseTimestamp + index);
});
const extractedPageArrays = await Promise.all(extractionPromises);
// Flatten all extracted pages
for (const pages of extractedPageArrays) {
allNewPages.push(...pages);
}
if (allNewPages.length === 0) return;
// Find insertion point (after the specified page)
const insertIndex = this.insertAfterPageNumber; // Insert after page N means insert at index N
// Create new pages array with inserted pages
const newPages: PDFPage[] = [];
let pageNumberCounter = 1;
// Add pages before insertion point
for (let i = 0; i < insertIndex && i < currentDoc.pages.length; i++) {
const page = { ...currentDoc.pages[i], pageNumber: pageNumberCounter++ };
newPages.push(page);
}
// Add inserted pages
for (const newPage of allNewPages) {
const insertedPage: PDFPage = {
...newPage,
pageNumber: pageNumberCounter++,
selected: false,
splitAfter: false
};
newPages.push(insertedPage);
this.insertedPages.push(insertedPage);
}
// Add remaining pages after insertion point
for (let i = insertIndex; i < currentDoc.pages.length; i++) {
const page = { ...currentDoc.pages[i], pageNumber: pageNumberCounter++ };
newPages.push(page);
}
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(updatedDocument);
// Update FileContext with the new document structure and inserted files
if (this.updateFileContext) {
this.updateFileContext(updatedDocument, this.insertedFileMap);
}
// Generate thumbnails for inserted pages (all files should be read by now)
this.generateThumbnailsForInsertedPages(updatedDocument);
// Maintain existing selection by mapping original selected pages to their new positions
const originalSelection = this.getSelectedPages();
const updatedSelection: number[] = [];
originalSelection.forEach(originalPageNum => {
if (originalPageNum <= this.insertAfterPageNumber) {
// Pages before insertion point keep same number
updatedSelection.push(originalPageNum);
} else {
// Pages after insertion point are shifted by number of inserted pages
updatedSelection.push(originalPageNum + allNewPages.length);
}
});
this.setSelectedPages(updatedSelection);
} catch (error) {
console.error('Failed to insert files:', error);
// Revert to original state if error occurs
if (this.originalDocument) {
this.setDocument(this.originalDocument);
}
}
}
private async generateThumbnailsForInsertedPages(updatedDocument: PDFDocument): Promise<void> {
try {
const { thumbnailGenerationService } = await import('../../../services/thumbnailGenerationService');
// Group pages by file ID to generate thumbnails efficiently
const pagesByFileId = new Map<FileId, PDFPage[]>();
for (const page of this.insertedPages) {
const fileId = page.id.substring(0, page.id.lastIndexOf('-page-')) as FileId /* FIX ME: This looks wrong - like we've thrown away info too early and need to recreate it */;
if (!pagesByFileId.has(fileId)) {
pagesByFileId.set(fileId, []);
}
pagesByFileId.get(fileId)!.push(page);
}
// Generate thumbnails for each file
for (const [fileId, pages] of pagesByFileId) {
const arrayBuffer = this.fileDataMap.get(fileId);
console.log('Generating thumbnails for file:', fileId);
console.log('Pages:', pages.length);
console.log('ArrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
if (arrayBuffer && arrayBuffer.byteLength > 0) {
// Extract page numbers for all pages from this file
const pageNumbers = pages.map(page => {
const pageNumMatch = page.id.match(/-page-(\d+)$/);
return pageNumMatch ? parseInt(pageNumMatch[1]) : 1;
});
console.log('Generating thumbnails for page numbers:', pageNumbers);
// Generate thumbnails for all pages from this file at once
const results = await thumbnailGenerationService.generateThumbnails(
fileId,
arrayBuffer,
pageNumbers,
{ scale: 0.2, quality: 0.8 }
);
console.log('Thumbnail generation results:', results.length, 'thumbnails generated');
// Update pages with generated thumbnails
for (let i = 0; i < results.length && i < pages.length; i++) {
const result = results[i];
const page = pages[i];
if (result.success) {
const pageIndex = updatedDocument.pages.findIndex(p => p.id === page.id);
if (pageIndex >= 0) {
updatedDocument.pages[pageIndex].thumbnail = result.thumbnail;
console.log('Updated thumbnail for page:', page.id);
}
}
}
// Trigger re-render by updating the document
this.setDocument({ ...updatedDocument });
} else {
console.error('No valid ArrayBuffer found for file ID:', fileId);
}
}
} catch (error) {
console.error('Failed to generate thumbnails for inserted pages:', error);
}
}
private async extractPagesFromFile(file: File, baseTimestamp: number): Promise<PDFPage[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) => {
try {
const arrayBuffer = event.target?.result as ArrayBuffer;
console.log('File reader onload - arrayBuffer size:', arrayBuffer?.byteLength || 'undefined');
if (!arrayBuffer) {
reject(new Error('Failed to read file'));
return;
}
// Clone the ArrayBuffer before passing to PDF.js (it might consume it)
const clonedArrayBuffer = arrayBuffer.slice(0);
// Use PDF.js via the worker manager to extract pages
const { pdfWorkerManager } = await import('../../../services/pdfWorkerManager');
const pdf = await pdfWorkerManager.createDocument(clonedArrayBuffer);
const pageCount = pdf.numPages;
const pages: PDFPage[] = [];
const fileId = `inserted-${file.name}-${baseTimestamp}` as FileId;
console.log('Original ArrayBuffer size:', arrayBuffer.byteLength);
console.log('Storing ArrayBuffer for fileId:', fileId, 'size:', arrayBuffer.byteLength);
// Store the original ArrayBuffer for thumbnail generation
this.fileDataMap.set(fileId, arrayBuffer);
console.log('After storing - fileDataMap size:', this.fileDataMap.size);
console.log('Stored value size:', this.fileDataMap.get(fileId)?.byteLength || 'undefined');
for (let i = 1; i <= pageCount; i++) {
const pageId = `${fileId}-page-${i}`;
pages.push({
id: pageId,
pageNumber: i, // Will be renumbered in execute()
originalPageNumber: i,
thumbnail: null, // Will be generated after insertion
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: false
});
}
// Clean up PDF document
pdfWorkerManager.destroyDocument(pdf);
resolve(pages);
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file);
});
}
undo(): void {
if (!this.originalDocument) return;
this.setDocument(this.originalDocument);
}
get description(): string {
return `Insert ${this.files.length} file(s) after page ${this.insertAfterPageNumber}`;
}
}
// Simple undo manager for DOM commands
export class UndoManager {
private undoStack: DOMCommand[] = [];
private redoStack: DOMCommand[] = [];
private onStateChange?: () => void;
setStateChangeCallback(callback: () => void): void {
this.onStateChange = callback;
}
executeCommand(command: DOMCommand): void {
command.execute();
this.undoStack.push(command);
this.redoStack = [];
this.onStateChange?.();
}
// For async commands that need to be executed manually
addToUndoStack(command: DOMCommand): void {
this.undoStack.push(command);
this.redoStack = [];
this.onStateChange?.();
}
undo(): boolean {
const command = this.undoStack.pop();
if (command) {
command.undo();
this.redoStack.push(command);
this.onStateChange?.();
return true;
}
return false;
}
redo(): boolean {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.undoStack.push(command);
this.onStateChange?.();
return true;
}
return false;
}
canUndo(): boolean {
return this.undoStack.length > 0;
}
canRedo(): boolean {
return this.redoStack.length > 0;
}
clear(): void {
this.undoStack = [];
this.redoStack = [];
this.onStateChange?.();
}
}
@@ -0,0 +1,8 @@
// Shared constants for PageEditor grid layout
export const GRID_CONSTANTS = {
ITEM_WIDTH: '20rem', // page width
ITEM_HEIGHT: '21.5rem', // 20rem + 1.5rem gap
ITEM_GAP: '1.5rem', // gap between items
OVERSCAN_SMALL: 4, // Overscan for normal documents
OVERSCAN_LARGE: 8, // Overscan for large documents (>1000 pages)
} as const;
@@ -0,0 +1,174 @@
import { useMemo } from 'react';
import { useFileState } from '../../../contexts/FileContext';
import { PDFDocument, PDFPage } from '../../../types/pageEditor';
import { FileId } from '../../../types/file';
export interface PageDocumentHook {
document: PDFDocument | null;
isVeryLargeDocument: boolean;
isLoading: boolean;
}
/**
* Hook for managing PDF document state and metadata in PageEditor
* Handles document merging, large document detection, and loading states
*/
export function usePageDocument(): PageDocumentHook {
const { state, selectors } = useFileState();
// Prefer IDs + selectors to avoid array identity churn
const activeFileIds = state.files.ids;
const primaryFileId = activeFileIds[0] ?? null;
// Stable signature for effects (prevents loops)
const filesSignature = selectors.getFilesSignature();
// UI state
const globalProcessing = state.ui.isProcessing;
// Get primary file record outside useMemo to track processedFile changes
const primaryStirlingFileStub = primaryFileId ? selectors.getStirlingFileStub(primaryFileId) : null;
const processedFilePages = primaryStirlingFileStub?.processedFile?.pages;
const processedFileTotalPages = primaryStirlingFileStub?.processedFile?.totalPages;
// Compute merged document with stable signature (prevents infinite loops)
const mergedPdfDocument = useMemo((): PDFDocument | null => {
if (activeFileIds.length === 0) return null;
const primaryFile = primaryFileId ? selectors.getFile(primaryFileId) : null;
// If we have file IDs but no file record, something is wrong - return null to show loading
if (!primaryStirlingFileStub) {
console.log('🎬 PageEditor: No primary file record found, showing loading');
return null;
}
const name =
activeFileIds.length === 1
? (primaryStirlingFileStub.name ?? 'document.pdf')
: activeFileIds
.map(id => (selectors.getStirlingFileStub(id)?.name ?? 'file').replace(/\.pdf$/i, ''))
.join(' + ');
// Build page insertion map from files with insertion positions
const insertionMap = new Map<string, FileId[]>(); // insertAfterPageId -> fileIds
const originalFileIds: FileId[] = [];
activeFileIds.forEach(fileId => {
const record = selectors.getStirlingFileStub(fileId);
if (record?.insertAfterPageId !== undefined) {
if (!insertionMap.has(record.insertAfterPageId)) {
insertionMap.set(record.insertAfterPageId, []);
}
insertionMap.get(record.insertAfterPageId)!.push(fileId);
} else {
originalFileIds.push(fileId);
}
});
// Build pages by interleaving original pages with insertions
let pages: PDFPage[] = [];
// Helper function to create pages from a file
const createPagesFromFile = (fileId: FileId, startPageNumber: number): PDFPage[] => {
const stirlingFileStub = selectors.getStirlingFileStub(fileId);
if (!stirlingFileStub) {
return [];
}
const processedFile = stirlingFileStub.processedFile;
let filePages: PDFPage[] = [];
if (processedFile?.pages && processedFile.pages.length > 0) {
// Use fully processed pages with thumbnails
filePages = processedFile.pages.map((page, pageIndex) => ({
id: `${fileId}-${page.pageNumber}`,
pageNumber: startPageNumber + pageIndex,
thumbnail: page.thumbnail || null,
rotation: page.rotation || 0,
selected: false,
splitAfter: page.splitAfter || false,
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
originalFileId: fileId,
}));
} else if (processedFile?.totalPages) {
// Fallback: create pages without thumbnails but with correct count
filePages = Array.from({ length: processedFile.totalPages }, (_, pageIndex) => ({
id: `${fileId}-${pageIndex + 1}`,
pageNumber: startPageNumber + pageIndex,
originalPageNumber: pageIndex + 1,
originalFileId: fileId,
rotation: 0,
thumbnail: null,
selected: false,
splitAfter: false,
}));
}
return filePages;
};
// Collect all pages from original files (without renumbering yet)
const originalFilePages: PDFPage[] = [];
originalFileIds.forEach(fileId => {
const filePages = createPagesFromFile(fileId, 1); // Temporary numbering
originalFilePages.push(...filePages);
});
// Start with all original pages numbered sequentially
pages = originalFilePages.map((page, index) => ({
...page,
pageNumber: index + 1
}));
// Process each insertion by finding the page ID and inserting after it
for (const [insertAfterPageId, fileIds] of insertionMap.entries()) {
const targetPageIndex = pages.findIndex(p => p.id === insertAfterPageId);
if (targetPageIndex === -1) continue;
// Collect all pages to insert
const allNewPages: PDFPage[] = [];
fileIds.forEach(fileId => {
const insertedPages = createPagesFromFile(fileId, 1);
allNewPages.push(...insertedPages);
});
// Insert all new pages after the target page
pages.splice(targetPageIndex + 1, 0, ...allNewPages);
// Renumber all pages after insertion
pages.forEach((page, index) => {
page.pageNumber = index + 1;
});
}
if (pages.length === 0) {
return null;
}
const mergedDoc: PDFDocument = {
id: activeFileIds.join('-'),
name,
file: primaryFile!,
pages,
totalPages: pages.length,
};
return mergedDoc;
}, [activeFileIds, primaryFileId, primaryStirlingFileStub, processedFilePages, processedFileTotalPages, selectors, filesSignature]);
// Large document detection for smart loading
const isVeryLargeDocument = useMemo(() => {
return mergedPdfDocument ? mergedPdfDocument.totalPages > 2000 : false;
}, [mergedPdfDocument?.totalPages]);
// Loading state
const isLoading = globalProcessing && !mergedPdfDocument;
return {
document: mergedPdfDocument,
isVeryLargeDocument,
isLoading
};
}
@@ -0,0 +1,95 @@
import { useState, useCallback } from 'react';
export interface PageEditorState {
// Selection state
selectionMode: boolean;
selectedPageIds: string[];
// Animation state
movingPage: number | null;
isAnimating: boolean;
// Split state
splitPositions: Set<number>;
// Export state
exportLoading: boolean;
// Actions
setSelectionMode: (mode: boolean) => void;
setSelectedPageIds: (pages: string[]) => void;
setMovingPage: (pageNumber: number | null) => void;
setIsAnimating: (animating: boolean) => void;
setSplitPositions: (positions: Set<number>) => void;
setExportLoading: (loading: boolean) => void;
// Helper functions
togglePage: (pageId: string) => void;
toggleSelectAll: (allPageIds: string[]) => void;
animateReorder: () => void;
}
/**
* Hook for managing PageEditor UI state
* Handles selection, animation, splits, and export states
*/
export function usePageEditorState(): PageEditorState {
// Selection state
const [selectionMode, setSelectionMode] = useState(false);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
// Animation state
const [movingPage, setMovingPage] = useState<number | null>(null);
const [isAnimating, setIsAnimating] = useState(false);
// Split state - position-based split tracking (replaces page-based splitAfter)
const [splitPositions, setSplitPositions] = useState<Set<number>>(new Set());
// Export state
const [exportLoading, setExportLoading] = useState(false);
// Helper functions
const togglePage = useCallback((pageId: string) => {
setSelectedPageIds(prev =>
prev.includes(pageId)
? prev.filter(id => id !== pageId)
: [...prev, pageId]
);
}, []);
const toggleSelectAll = useCallback((allPageIds: string[]) => {
if (!allPageIds.length) return;
setSelectedPageIds(prev =>
prev.length === allPageIds.length ? [] : allPageIds
);
}, []);
const animateReorder = useCallback(() => {
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 500);
}, []);
return {
// State
selectionMode,
selectedPageIds,
movingPage,
isAnimating,
splitPositions,
exportLoading,
// Setters
setSelectionMode,
setSelectedPageIds,
setMovingPage,
setIsAnimating,
setSplitPositions,
setExportLoading,
// Helpers
togglePage,
toggleSelectAll,
animateReorder,
};
}
@@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next';
import { Tooltip } from './Tooltip';
import AppsIcon from '@mui/icons-material/AppsRounded';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useSidebarNavigation } from '../../hooks/useSidebarNavigation';
import { handleUnlessSpecialClick } from '../../utils/clickHandlers';
interface AllToolsNavButtonProps {
activeButton: string;
@@ -13,6 +15,7 @@ interface AllToolsNavButtonProps {
const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, setActiveButton }) => {
const { t } = useTranslation();
const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow();
const { getHomeNavigation } = useSidebarNavigation();
const handleClick = () => {
setActiveButton('tools');
@@ -24,25 +27,34 @@ const AllToolsNavButton: React.FC<AllToolsNavButtonProps> = ({ activeButton, set
// Do not highlight All Tools when a specific tool is open (indicator is shown)
const isActive = activeButton === 'tools' && !selectedToolKey && leftPanelView === 'toolPicker';
const navProps = getHomeNavigation();
const handleNavClick = (e: React.MouseEvent) => {
handleUnlessSpecialClick(e, handleClick);
};
const iconNode = (
<span className="iconContainer">
<AppsIcon sx={{ fontSize: '1.5rem' }} />
<AppsIcon sx={{ fontSize: '2rem' }} />
</span>
);
return (
<Tooltip content={t("quickAccess.allTools", "All Tools")} position="right" arrow containerStyle={{ marginTop: "-1rem" }} maxWidth={200}>
<div className="flex flex-col items-center gap-1 mt-4 mb-2">
<ActionIcon
component="a"
href={navProps.href}
onClick={handleNavClick}
size={'lg'}
variant="subtle"
onClick={handleClick}
aria-label={t("quickAccess.allTools", "All Tools")}
style={{
backgroundColor: isActive ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)',
color: isActive ? 'var(--icon-tools-color)' : 'var(--icon-inactive-color)',
border: 'none',
borderRadius: '8px',
textDecoration: 'none'
}}
className={isActive ? 'activeIconScale' : ''}
>
@@ -0,0 +1,216 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import ButtonSelector from './ButtonSelector';
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('ButtonSelector', () => {
const mockOnChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render all options as buttons', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
label="Test Label"
/>
</TestWrapper>
);
expect(screen.getByText('Test Label')).toBeInTheDocument();
expect(screen.getByText('Option 1')).toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});
test('should highlight selected button with filled variant', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
label="Selection Label"
/>
</TestWrapper>
);
const selectedButton = screen.getByRole('button', { name: 'Option 1' });
const unselectedButton = screen.getByRole('button', { name: 'Option 2' });
// Check data-variant attribute for filled/outline
expect(selectedButton).toHaveAttribute('data-variant', 'filled');
expect(unselectedButton).toHaveAttribute('data-variant', 'outline');
expect(screen.getByText('Selection Label')).toBeInTheDocument();
});
test('should call onChange when button is clicked', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
expect(mockOnChange).toHaveBeenCalledWith('option2');
});
test('should handle undefined value (no selection)', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value={undefined}
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
// Both buttons should be outlined when no value is selected
const button1 = screen.getByRole('button', { name: 'Option 1' });
const button2 = screen.getByRole('button', { name: 'Option 2' });
expect(button1).toHaveAttribute('data-variant', 'outline');
expect(button2).toHaveAttribute('data-variant', 'outline');
});
test.each([
{
description: 'disable buttons when disabled prop is true',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
],
globalDisabled: true,
expectedStates: [true, true],
},
{
description: 'disable individual options when option.disabled is true',
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2', disabled: true },
],
globalDisabled: false,
expectedStates: [false, true],
},
])('should $description', ({ options, globalDisabled, expectedStates }) => {
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
disabled={globalDisabled}
/>
</TestWrapper>
);
options.forEach((option, index) => {
const button = screen.getByRole('button', { name: option.label });
expect(button).toHaveProperty('disabled', expectedStates[index]);
});
});
test('should not call onChange when disabled button is clicked', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2', disabled: true },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
expect(mockOnChange).not.toHaveBeenCalled();
});
test('should not apply fullWidth styling when fullWidth is false', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
fullWidth={false}
label="Layout Label"
/>
</TestWrapper>
);
const button = screen.getByRole('button', { name: 'Option 1' });
expect(button).not.toHaveStyle({ flex: '1' });
expect(screen.getByText('Layout Label')).toBeInTheDocument();
});
test('should not render label element when not provided', () => {
const options = [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
];
const { container } = render(
<TestWrapper>
<ButtonSelector
value="option1"
onChange={mockOnChange}
options={options}
/>
</TestWrapper>
);
// Should render buttons
expect(screen.getByText('Option 1')).toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
// Stack should only contain the Group (buttons), no Text element for label
const stackElement = container.querySelector('[class*="mantine-Stack-root"]');
expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text
});
});
@@ -0,0 +1,59 @@
import { Button, Group, Stack, Text } from "@mantine/core";
export interface ButtonOption<T> {
value: T;
label: string;
disabled?: boolean;
}
interface ButtonSelectorProps<T> {
value: T | undefined;
onChange: (value: T) => void;
options: ButtonOption<T>[];
label?: string;
disabled?: boolean;
fullWidth?: boolean;
}
const ButtonSelector = <T extends string>({
value,
onChange,
options,
label = undefined,
disabled = false,
fullWidth = true,
}: ButtonSelectorProps<T>) => {
return (
<Stack gap='var(--mantine-spacing-sm)'>
{/* Label (if it exists) */}
{label && <Text style={{
fontSize: "var(--mantine-font-size-sm)",
lineHeight: "var(--mantine-line-height-sm)",
fontWeight: "var(--font-weight-medium)",
}}>{label}</Text>}
{/* Buttons */}
<Group gap='4px'>
{options.map((option) => (
<Button
key={option.value}
variant={value === option.value ? 'filled' : 'outline'}
color={value === option.value ? 'var(--color-primary-500)' : 'var(--text-muted)'}
onClick={() => onChange(option.value)}
disabled={disabled || option.disabled}
style={{
flex: fullWidth ? 1 : undefined,
height: 'auto',
minHeight: '2.5rem',
fontSize: 'var(--mantine-font-size-sm)'
}}
>
{option.label}
</Button>
))}
</Group>
</Stack>
);
};
export default ButtonSelector;
@@ -0,0 +1,99 @@
import { Stack, Card, Text, Flex } from '@mantine/core';
import { Tooltip } from './Tooltip';
import { useTranslation } from 'react-i18next';
export interface CardOption<T = string> {
value: T;
prefixKey: string;
nameKey: string;
tooltipKey?: string;
tooltipContent?: any[];
}
export interface CardSelectorProps<T, K extends CardOption<T>> {
options: K[];
onSelect: (value: T) => void;
disabled?: boolean;
getTooltipContent?: (option: K) => any[];
}
const CardSelector = <T, K extends CardOption<T>>({
options,
onSelect,
disabled = false,
getTooltipContent
}: CardSelectorProps<T, K>) => {
const { t } = useTranslation();
const handleOptionClick = (value: T) => {
if (!disabled) {
onSelect(value);
}
};
const getTooltips = (option: K) => {
if (getTooltipContent) {
return getTooltipContent(option);
}
return [];
};
return (
<Stack gap="sm">
{options.map((option) => (
<Tooltip
key={option.value as string}
sidebarTooltip
tips={getTooltips(option)}
>
<Card
radius="md"
w="100%"
h={'2.8rem'}
style={{
cursor: disabled ? 'default' : 'pointer',
backgroundColor: 'var(--mantine-color-gray-2)',
borderColor: 'var(--mantine-color-gray-3)',
opacity: disabled ? 0.6 : 1,
display: 'flex',
flexDirection: 'row',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (!disabled) {
e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-3)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}}
onMouseLeave={(e) => {
if (!disabled) {
e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-2)';
e.currentTarget.style.transform = 'translateY(0px)';
e.currentTarget.style.boxShadow = 'none';
}
}}
onClick={() => handleOptionClick(option.value)}
>
<Flex align={'center'} pl="sm" w="100%">
<Text size="sm" c="dimmed" ta="center" fw={350}>
{t(option.prefixKey, "Prefix")}
</Text>
<Text
fw={600}
size="sm"
c={undefined}
ta="center"
style={{ marginLeft: '0.25rem' }}
>
{t(option.nameKey, "Option Name")}
</Text>
</Flex>
</Card>
</Tooltip>
))}
</Stack>
);
};
export default CardSelector;
@@ -0,0 +1,56 @@
import React from 'react';
import { Text, Button, Stack } from '@mantine/core';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
fallback?: React.ComponentType<{error?: Error; retry: () => void}>;
}
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
retry = () => {
this.setState({ hasError: false, error: undefined });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
const Fallback = this.props.fallback;
return <Fallback error={this.state.error} retry={this.retry} />;
}
return (
<Stack align="center" justify="center" style={{ minHeight: '200px', padding: '2rem' }}>
<Text size="lg" fw={500} c="red">Something went wrong</Text>
{process.env.NODE_ENV === 'development' && this.state.error && (
<Text size="sm" c="dimmed" style={{ textAlign: 'center', fontFamily: 'monospace' }}>
{this.state.error.message}
</Text>
)}
<Button onClick={this.retry} variant="light">
Try Again
</Button>
</Stack>
);
}
return this.props.children;
}
}
+8 -5
View File
@@ -6,12 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditIcon from "@mui/icons-material/Edit";
import { FileWithUrl } from "../../types/file";
import { StirlingFileStub } from "../../types/fileContext";
import { getFileSize, getFileDate } from "../../utils/fileUtils";
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
interface FileCardProps {
file: FileWithUrl;
file: File;
fileStub?: StirlingFileStub;
onRemove: () => void;
onDoubleClick?: () => void;
onView?: () => void;
@@ -21,9 +22,11 @@ interface FileCardProps {
isSupported?: boolean; // Whether the file format is supported by the current tool
}
const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
const { t } = useTranslation();
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
// Use record thumbnail if available, otherwise fall back to IndexedDB lookup
const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileStub);
const thumb = fileStub?.thumbnailUrl || indexedDBThumb;
const [isHovered, setIsHovered] = useState(false);
return (
@@ -173,7 +176,7 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
<Badge color="blue" variant="light" size="sm">
{getFileDate(file)}
</Badge>
{file.storedInIndexedDB && (
{fileStub?.id && (
<Badge
color="green"
variant="light"
+33 -23
View File
@@ -1,19 +1,20 @@
import React, { useState } from "react";
import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@mantine/core";
import { useState } from "react";
import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard";
import { FileWithUrl } from "../../types/file";
import { StirlingFileStub } from "../../types/fileContext";
import { FileId } from "../../types/file";
interface FileGridProps {
files: FileWithUrl[];
files: Array<{ file: File; record?: StirlingFileStub }>;
onRemove?: (index: number) => void;
onDoubleClick?: (file: FileWithUrl) => void;
onView?: (file: FileWithUrl) => void;
onEdit?: (file: FileWithUrl) => void;
onSelect?: (fileId: string) => void;
selectedFiles?: string[];
onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void;
onView?: (item: { file: File; record?: StirlingFileStub }) => void;
onEdit?: (item: { file: File; record?: StirlingFileStub }) => void;
onSelect?: (fileId: FileId) => void;
selectedFiles?: FileId[];
showSearch?: boolean;
showSort?: boolean;
maxDisplay?: number; // If set, shows only this many files with "Show All" option
@@ -46,19 +47,19 @@ const FileGrid = ({
const [sortBy, setSortBy] = useState<SortOption>('date');
// Filter files based on search term
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
const filteredFiles = files.filter(item =>
item.file.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Sort files
const sortedFiles = [...filteredFiles].sort((a, b) => {
switch (sortBy) {
case 'date':
return (b.lastModified || 0) - (a.lastModified || 0);
return (b.file.lastModified || 0) - (a.file.lastModified || 0);
case 'name':
return a.name.localeCompare(b.name);
return a.file.name.localeCompare(b.file.name);
case 'size':
return (b.size || 0) - (a.size || 0);
return (b.file.size || 0) - (a.file.size || 0);
default:
return 0;
}
@@ -119,21 +120,30 @@ const FileGrid = ({
direction="row"
wrap="wrap"
gap="md"
h="30rem"
h="30rem"
style={{ overflowY: "auto", width: "100%" }}
>
{displayFiles.map((file, idx) => {
const fileId = file.id || file.name;
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
const supported = isFileSupported ? isFileSupported(file.name) : true;
{displayFiles
.filter(item => {
if (!item.record?.id) {
console.error('FileGrid: File missing StirlingFileStub with proper ID:', item.file.name);
return false;
}
return true;
})
.map((item, idx) => {
const fileId = item.record!.id; // Safe to assert after filter
const originalIdx = files.findIndex(f => f.record?.id === fileId);
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
return (
<FileCard
key={fileId + idx}
file={file}
file={item.file}
fileStub={item.record}
onRemove={onRemove ? () => onRemove(originalIdx) : () => {}}
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
onView={onView && supported ? () => onView(file) : undefined}
onEdit={onEdit && supported ? () => onEdit(file) : undefined}
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(item) : undefined}
onView={onView && supported ? () => onView(item) : undefined}
onEdit={onEdit && supported ? () => onEdit(item) : undefined}
isSelected={selectedFiles.includes(fileId)}
onSelect={onSelect && supported ? () => onSelect(fileId) : undefined}
isSupported={supported}
@@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
Text,
Button,
Group,
Stack,
Checkbox,
ScrollArea,
import {
Modal,
Text,
Button,
Group,
Stack,
Checkbox,
ScrollArea,
Box,
Image,
Badge,
@@ -15,11 +15,12 @@ import {
} from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next';
import { FileId } from '../../types/file';
interface FilePickerModalProps {
opened: boolean;
onClose: () => void;
storedFiles: any[]; // Files from storage (FileWithUrl format)
storedFiles: any[]; // Files from storage (various formats supported)
onSelectFiles: (selectedFiles: File[]) => void;
}
@@ -30,7 +31,7 @@ const FilePickerModal = ({
onSelectFiles,
}: FilePickerModalProps) => {
const { t } = useTranslation();
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
// Reset selection when modal opens
useEffect(() => {
@@ -39,16 +40,16 @@ const FilePickerModal = ({
}
}, [opened]);
const toggleFileSelection = (fileId: string) => {
const toggleFileSelection = (fileId: FileId) => {
setSelectedFileIds(prev => {
return prev.includes(fileId)
return prev.includes(fileId)
? prev.filter(id => id !== fileId)
: [...prev, fileId];
});
};
const selectAll = () => {
setSelectedFileIds(storedFiles.map(f => f.id || f.name));
setSelectedFileIds(storedFiles.map(f => f.id).filter(Boolean));
};
const selectNone = () => {
@@ -56,10 +57,10 @@ const FilePickerModal = ({
};
const handleConfirm = async () => {
const selectedFiles = storedFiles.filter(f =>
selectedFileIds.includes(f.id || f.name)
const selectedFiles = storedFiles.filter(f =>
selectedFileIds.includes(f.id)
);
// Convert stored files to File objects
const convertedFiles = await Promise.all(
selectedFiles.map(async (fileItem) => {
@@ -68,12 +69,12 @@ const FilePickerModal = ({
if (fileItem instanceof File) {
return fileItem;
}
// If it has a file property, use that
if (fileItem.file && fileItem.file instanceof File) {
return fileItem.file;
}
// If it's from IndexedDB storage, reconstruct the File
if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') {
const arrayBuffer = await fileItem.arrayBuffer();
@@ -83,8 +84,8 @@ const FilePickerModal = ({
lastModified: fileItem.lastModified || Date.now()
});
}
// If it has data property, reconstruct the File
// If it has data property, reconstruct the File
if (fileItem.data) {
const blob = new Blob([fileItem.data], { type: fileItem.type || 'application/pdf' });
return new File([blob], fileItem.name, {
@@ -92,7 +93,7 @@ const FilePickerModal = ({
lastModified: fileItem.lastModified || Date.now()
});
}
console.warn('Could not convert file item:', fileItem);
return null;
} catch (error) {
@@ -101,10 +102,10 @@ const FilePickerModal = ({
}
})
);
// Filter out any null values and return valid Files
const validFiles = convertedFiles.filter((f): f is File => f !== null);
onSelectFiles(validFiles);
onClose();
};
@@ -154,20 +155,20 @@ const FilePickerModal = ({
<ScrollArea.Autosize mah={400}>
<SimpleGrid cols={2} spacing="md">
{storedFiles.map((file) => {
const fileId = file.id || file.name;
const fileId = file.id;
const isSelected = selectedFileIds.includes(fileId);
return (
<Box
key={fileId}
p="sm"
style={{
border: isSelected
? '2px solid var(--mantine-color-blue-6)'
border: isSelected
? '2px solid var(--mantine-color-blue-6)'
: '1px solid var(--mantine-color-gray-3)',
borderRadius: 8,
backgroundColor: isSelected
? 'var(--mantine-color-blue-0)'
backgroundColor: isSelected
? 'var(--mantine-color-blue-0)'
: 'transparent',
cursor: 'pointer',
transition: 'all 0.2s ease'
@@ -180,7 +181,7 @@ const FilePickerModal = ({
onChange={() => toggleFileSelection(fileId)}
onClick={(e) => e.stopPropagation()}
/>
{/* Thumbnail */}
<Box
style={{
@@ -246,11 +247,11 @@ const FilePickerModal = ({
<Button variant="light" onClick={onClose}>
{t("close", "Cancel")}
</Button>
<Button
<Button
onClick={handleConfirm}
disabled={selectedFileIds.length === 0}
>
{selectedFileIds.length > 0
{selectedFileIds.length > 0
? `${t("fileUpload.loadFromStorage", "Load")} ${selectedFileIds.length} ${t("fileUpload.uploadFiles", "Files")}`
: t("fileUpload.loadFromStorage", "Load Files")
}
@@ -261,4 +262,4 @@ const FilePickerModal = ({
);
};
export default FilePickerModal;
export default FilePickerModal;
+20 -5
View File
@@ -1,6 +1,7 @@
import React from 'react';
import { Box } from '@mantine/core';
import { FileWithUrl } from '../../types/file';
import { Box, Center } from '@mantine/core';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import { StirlingFileStub } from '../../types/fileContext';
import DocumentThumbnail from './filePreview/DocumentThumbnail';
import DocumentStack from './filePreview/DocumentStack';
import HoverOverlay from './filePreview/HoverOverlay';
@@ -8,7 +9,7 @@ import NavigationArrows from './filePreview/NavigationArrows';
export interface FilePreviewProps {
// Core file data
file: File | FileWithUrl | null;
file: File | StirlingFileStub | null;
thumbnail?: string | null;
// Optional features
@@ -21,7 +22,7 @@ export interface FilePreviewProps {
isAnimating?: boolean;
// Event handlers
onFileClick?: (file: File | FileWithUrl | null) => void;
onFileClick?: (file: File | StirlingFileStub | null) => void;
onPrevious?: () => void;
onNext?: () => void;
}
@@ -38,7 +39,21 @@ const FilePreview: React.FC<FilePreviewProps> = ({
onPrevious,
onNext
}) => {
if (!file) return null;
if (!file) {
return (
<Box style={{ width: '100%', height: '100%' }}>
<Center style={{ width: '100%', height: '100%' }}>
<InsertDriveFileIcon
style={{
fontSize: '4rem',
color: 'var(--mantine-color-gray-4)',
opacity: 0.6
}}
/>
</Center>
</Box>
);
}
const hasMultipleFiles = totalFiles > 1;

Some files were not shown because too many files have changed in this diff Show More