mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-10 01:14:17 +01:00
feat: translate all pages, UI messages and docs and fix SEO redirects and indexing
This commit is contained in:
1 parent
5e019de5ae
commit
4edb1718cc
3344 files changed
+415643
-4759
No files matched your search
@@ -8,6 +8,8 @@ import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
|
||||
import type { CropBox } from '@/types';
|
||||
import './utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText } from './i18n/i18n.js';
|
||||
|
||||
const editorState: {
|
||||
pdf: PDFDocumentProxy | null;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
@@ -61,7 +63,7 @@ function calculateFitScale(page: PDFPageProxy) {
|
||||
*/
|
||||
async function renderPage(num: number) {
|
||||
editorState.pageRendering = true;
|
||||
showLoader(`Loading page ${num}...`);
|
||||
showLoader(trText('Loading page {{num}}...', { num }));
|
||||
|
||||
try {
|
||||
const page = await editorState.pdf!.getPage(num);
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
CompareTextChange,
|
||||
} from '../types.ts';
|
||||
|
||||
import { trText } from '../../i18n/i18n.js';
|
||||
|
||||
const HEADER_FOOTER_ZONE = 0.12;
|
||||
|
||||
export function classifyChangeCategory(
|
||||
@@ -130,7 +132,15 @@ export function diffImages(
|
||||
id: `image-modified-${idx++}`,
|
||||
type: 'modified',
|
||||
category: 'image',
|
||||
description: `Image resized from ${bImg.width}×${bImg.height} to ${match.width}×${match.height}`,
|
||||
description: trText(
|
||||
'Image resized from {{width}}×{{height}} to {{width2}}×{{height2}}',
|
||||
{
|
||||
width: bImg.width,
|
||||
height: bImg.height,
|
||||
width2: match.width,
|
||||
height2: match.height,
|
||||
}
|
||||
),
|
||||
beforeText: `${bImg.width}×${bImg.height}`,
|
||||
afterText: `${match.width}×${match.height}`,
|
||||
beforeRects: [bImg.rect],
|
||||
@@ -142,7 +152,10 @@ export function diffImages(
|
||||
id: `image-removed-${idx++}`,
|
||||
type: 'removed',
|
||||
category: 'image',
|
||||
description: `Removed image (${bImg.width}×${bImg.height})`,
|
||||
description: trText('Removed image ({{width}}×{{height}})', {
|
||||
width: bImg.width,
|
||||
height: bImg.height,
|
||||
}),
|
||||
beforeText: '',
|
||||
afterText: '',
|
||||
beforeRects: [bImg.rect],
|
||||
@@ -157,7 +170,10 @@ export function diffImages(
|
||||
id: `image-added-${idx++}`,
|
||||
type: 'added',
|
||||
category: 'image',
|
||||
description: `Added image (${aImg.width}×${aImg.height})`,
|
||||
description: trText('Added image ({{width}}×{{height}})', {
|
||||
width: aImg.width,
|
||||
height: aImg.height,
|
||||
}),
|
||||
beforeText: '',
|
||||
afterText: '',
|
||||
beforeRects: [],
|
||||
@@ -207,7 +223,7 @@ export function detectBackgroundChanges(
|
||||
id: `background-changed-${baseId}`,
|
||||
type: 'modified',
|
||||
category: 'background',
|
||||
description: 'Page background or layout changed',
|
||||
description: trText('Page background or layout changed'),
|
||||
beforeText: '',
|
||||
afterText: '',
|
||||
beforeRects: [
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
CompareCategorySummary,
|
||||
} from '../types.ts';
|
||||
import { diffTextRuns } from './diff-text-runs.ts';
|
||||
import { diffTextRunsAsync } from '../worker-api.ts';
|
||||
import { diffStrings, diffTextRunsAsync } from '../worker-api.ts';
|
||||
import {
|
||||
classifyChangeCategory,
|
||||
diffAnnotations,
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
buildCategorySummary,
|
||||
} from './compare-content.ts';
|
||||
|
||||
import { trText } from '../../i18n/i18n.js';
|
||||
|
||||
const EMPTY_CATEGORY_SUMMARY: CompareCategorySummary = {
|
||||
text: 0,
|
||||
image: 0,
|
||||
@@ -54,7 +56,10 @@ function comparePageModelsCore(
|
||||
id: 'page-removed',
|
||||
type: 'page-removed',
|
||||
category: 'text',
|
||||
description: `Page ${leftPage.pageNumber} exists only in the first PDF.`,
|
||||
description: trText(
|
||||
'Page {{pageNumber}} exists only in the first PDF.',
|
||||
{ pageNumber: leftPage.pageNumber }
|
||||
),
|
||||
beforeText: leftPage.plainText.slice(0, 200),
|
||||
afterText: '',
|
||||
beforeRects: [],
|
||||
@@ -78,7 +83,10 @@ function comparePageModelsCore(
|
||||
id: 'page-added',
|
||||
type: 'page-added',
|
||||
category: 'text',
|
||||
description: `Page ${rightPage.pageNumber} exists only in the second PDF.`,
|
||||
description: trText(
|
||||
'Page {{pageNumber}} exists only in the second PDF.',
|
||||
{ pageNumber: rightPage.pageNumber }
|
||||
),
|
||||
beforeText: '',
|
||||
afterText: rightPage.plainText.slice(0, 200),
|
||||
beforeRects: [],
|
||||
@@ -150,5 +158,7 @@ function comparePageModelsCore(
|
||||
);
|
||||
}
|
||||
|
||||
return buildResult(diffTextRuns(leftPage.textItems, rightPage.textItems));
|
||||
return buildResult(
|
||||
diffTextRuns(leftPage.textItems, rightPage.textItems, diffStrings())
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ interface DiffMessage {
|
||||
id: number;
|
||||
beforeItems: CompareTextItem[];
|
||||
afterItems: CompareTextItem[];
|
||||
strings: Record<string, string>;
|
||||
}
|
||||
|
||||
interface PairMessage {
|
||||
@@ -48,7 +49,12 @@ function isValidMessage(data: unknown): data is WorkerMessage {
|
||||
const m = data as Record<string, unknown>;
|
||||
if (typeof m.id !== 'number') return false;
|
||||
if (m.type === 'diff') {
|
||||
return Array.isArray(m.beforeItems) && Array.isArray(m.afterItems);
|
||||
return (
|
||||
Array.isArray(m.beforeItems) &&
|
||||
Array.isArray(m.afterItems) &&
|
||||
typeof m.strings === 'object' &&
|
||||
m.strings !== null
|
||||
);
|
||||
}
|
||||
if (m.type === 'pair') {
|
||||
return Array.isArray(m.leftPages) && Array.isArray(m.rightPages);
|
||||
@@ -68,7 +74,8 @@ self.onmessage = function (e: MessageEvent<unknown>) {
|
||||
if (msg.type === 'diff') {
|
||||
const { changes, summary } = diffTextRuns(
|
||||
msg.beforeItems,
|
||||
msg.afterItems
|
||||
msg.afterItems,
|
||||
msg.strings
|
||||
);
|
||||
const result: DiffResult = {
|
||||
type: 'diff',
|
||||
|
||||
@@ -15,6 +15,15 @@ import {
|
||||
} from './text-normalization.ts';
|
||||
import { COMPARE_GEOMETRY } from '../config.ts';
|
||||
|
||||
import { fillPlaceholders } from '../../i18n/placeholders.js';
|
||||
|
||||
let activeStrings: Record<string, string> = {};
|
||||
|
||||
const describeChange = (
|
||||
template: string,
|
||||
params: Record<string, string | number>
|
||||
): string => fillPlaceholders(activeStrings[template] ?? template, params);
|
||||
|
||||
interface WordToken {
|
||||
word: string;
|
||||
compareWord: string;
|
||||
@@ -174,7 +183,13 @@ function createWordChange(
|
||||
id,
|
||||
type,
|
||||
category: 'text',
|
||||
description: `Replaced "${beforeText}" with "${afterText}"`,
|
||||
description: describeChange(
|
||||
'Replaced "{{beforeText}}" with "{{afterText}}"',
|
||||
{
|
||||
beforeText,
|
||||
afterText,
|
||||
}
|
||||
),
|
||||
beforeText,
|
||||
afterText,
|
||||
beforeRects,
|
||||
@@ -185,7 +200,7 @@ function createWordChange(
|
||||
id,
|
||||
type,
|
||||
category: 'text',
|
||||
description: `Removed "${beforeText}"`,
|
||||
description: describeChange('Removed "{{beforeText}}"', { beforeText }),
|
||||
beforeText,
|
||||
afterText: '',
|
||||
beforeRects,
|
||||
@@ -196,7 +211,7 @@ function createWordChange(
|
||||
id,
|
||||
type,
|
||||
category: 'text',
|
||||
description: `Added "${afterText}"`,
|
||||
description: describeChange('Added "{{afterText}}"', { afterText }),
|
||||
beforeText: '',
|
||||
afterText,
|
||||
beforeRects: [],
|
||||
@@ -221,8 +236,10 @@ function toSummary(changes: CompareTextChange[]): CompareChangeSummary {
|
||||
|
||||
export function diffTextRuns(
|
||||
beforeItems: CompareTextItem[],
|
||||
afterItems: CompareTextItem[]
|
||||
afterItems: CompareTextItem[],
|
||||
strings: Record<string, string> = {}
|
||||
) {
|
||||
activeStrings = strings;
|
||||
const beforeWords = beforeItems.flatMap(splitLineIntoWords);
|
||||
const afterWords = afterItems.flatMap(splitLineIntoWords);
|
||||
|
||||
@@ -383,11 +400,18 @@ function detectStyleChanges(
|
||||
const allBeforeRects = groupFrags.flatMap((f) => f.beforeRects);
|
||||
const allAfterRects = groupFrags.flatMap((f) => f.afterRects);
|
||||
|
||||
let desc = `Style changed (${groupFrags.length} regions)`;
|
||||
let desc = describeChange('Style changed ({{length}} regions)', {
|
||||
length: groupFrags.length,
|
||||
});
|
||||
const details: string[] = [];
|
||||
if (bFont !== aFont) details.push(`Font: ${bFont} → ${aFont}`);
|
||||
if (bFont !== aFont)
|
||||
details.push(
|
||||
describeChange('Font: {{bFont}} → {{aFont}}', { bFont, aFont })
|
||||
);
|
||||
if (bSize && aSize && Math.abs(bSize - aSize) > 0.5)
|
||||
details.push(`Font size: ${bSize} → ${aSize}`);
|
||||
details.push(
|
||||
describeChange('Font size: {{bSize}} → {{aSize}}', { bSize, aSize })
|
||||
);
|
||||
if (details.length) desc += '\n' + details.map((d) => `• ${d}`).join('\n');
|
||||
|
||||
changes.push({
|
||||
@@ -457,7 +481,9 @@ function detectMovedText(changes: CompareTextChange[]) {
|
||||
id: `moved-${changes.length}`,
|
||||
type: 'moved',
|
||||
category: 'text',
|
||||
description: `Moved "${rem.beforeText.slice(0, 80)}"`,
|
||||
description: describeChange('Moved "{{value}}"', {
|
||||
value: rem.beforeText.slice(0, 80),
|
||||
}),
|
||||
beforeText: rem.beforeText,
|
||||
afterText: bestMatch.afterText,
|
||||
beforeRects: rem.beforeRects,
|
||||
|
||||
@@ -3,6 +3,8 @@ import pixelmatch from 'pixelmatch';
|
||||
import type { CompareVisualDiff } from '../types.ts';
|
||||
import { VISUAL_DIFF as VISUAL_DIFF_CONFIG } from '../config.ts';
|
||||
|
||||
import { trText } from '../../i18n/i18n.js';
|
||||
|
||||
type FocusRegion = {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -23,7 +25,7 @@ function drawNormalized(
|
||||
) {
|
||||
const context = targetCanvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('Could not create comparison canvas context.');
|
||||
throw new Error(trText('Could not create comparison canvas context.'));
|
||||
}
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
@@ -56,7 +58,7 @@ export function renderVisualDiff(
|
||||
const outputContext = outputCanvas.getContext('2d');
|
||||
|
||||
if (!context1 || !context2 || !outputContext) {
|
||||
throw new Error('Could not create visual diff context.');
|
||||
throw new Error(trText('Could not create visual diff context.'));
|
||||
}
|
||||
|
||||
const image1 = context1.getImageData(0, 0, width, height);
|
||||
@@ -87,7 +89,7 @@ export function renderVisualDiff(
|
||||
const overlayContext = overlayCanvas.getContext('2d');
|
||||
|
||||
if (!overlayContext) {
|
||||
throw new Error('Could not create visual diff overlay context.');
|
||||
throw new Error(trText('Could not create visual diff overlay context.'));
|
||||
}
|
||||
|
||||
overlayContext.putImageData(diffImage, 0, 0);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { computeComparisonForPair } from '../../logic/compare-render.ts';
|
||||
import { LRUCache } from '../lru-cache.ts';
|
||||
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
|
||||
|
||||
import { trText } from '../../i18n/i18n.js';
|
||||
|
||||
const HIGHLIGHT_COLORS: Record<
|
||||
string,
|
||||
{ r: number; g: number; b: number; opacity: number }
|
||||
@@ -99,10 +101,12 @@ export async function exportComparePdf(
|
||||
}
|
||||
) {
|
||||
if (!pdfDoc1 && !pdfDoc2) {
|
||||
throw new Error('At least one PDF document is required for export.');
|
||||
throw new Error(
|
||||
trText('At least one PDF document is required for export.')
|
||||
);
|
||||
}
|
||||
if (!pairs || pairs.length === 0) {
|
||||
throw new Error('No page pairs to export.');
|
||||
throw new Error(trText('No page pairs to export.'));
|
||||
}
|
||||
|
||||
const outDoc = await PDFDocument.create();
|
||||
@@ -139,7 +143,10 @@ export async function exportComparePdf(
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const pair = pairs[i];
|
||||
onProgress?.(
|
||||
`Rendering page ${i + 1} of ${pairs.length}...`,
|
||||
trText('Rendering page {{n}} of {{length}}...', {
|
||||
n: i + 1,
|
||||
length: pairs.length,
|
||||
}),
|
||||
Math.round(((i + 1) / pairs.length) * 100)
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,31 @@ import type {
|
||||
} from './types.ts';
|
||||
import { diffTextRuns } from './engine/diff-text-runs.ts';
|
||||
import { pairPages } from './engine/pair-pages.ts';
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
export function diffStrings(): Record<string, string> {
|
||||
const strings: Record<string, string> = {};
|
||||
const add = (english: string, translated: string) => {
|
||||
strings[english] = translated;
|
||||
};
|
||||
add(
|
||||
'Replaced "{{beforeText}}" with "{{afterText}}"',
|
||||
trText('Replaced "{{beforeText}}" with "{{afterText}}"')
|
||||
);
|
||||
add('Removed "{{beforeText}}"', trText('Removed "{{beforeText}}"'));
|
||||
add('Added "{{afterText}}"', trText('Added "{{afterText}}"'));
|
||||
add(
|
||||
'Style changed ({{length}} regions)',
|
||||
trText('Style changed ({{length}} regions)')
|
||||
);
|
||||
add('Font: {{bFont}} → {{aFont}}', trText('Font: {{bFont}} → {{aFont}}'));
|
||||
add(
|
||||
'Font size: {{bSize}} → {{aSize}}',
|
||||
trText('Font size: {{bSize}} → {{aSize}}')
|
||||
);
|
||||
add('Moved "{{value}}"', trText('Moved "{{value}}"'));
|
||||
return strings;
|
||||
}
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let messageId = 0;
|
||||
@@ -37,7 +62,7 @@ function getWorker(): Worker | null {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
for (const [, p] of pending) {
|
||||
p.reject(new Error('Worker crashed'));
|
||||
p.reject(new Error(trText('Worker crashed')));
|
||||
}
|
||||
pending.clear();
|
||||
};
|
||||
@@ -49,7 +74,7 @@ function getWorker(): Worker | null {
|
||||
|
||||
function postToWorker(msg: Record<string, unknown>): Promise<unknown> {
|
||||
const w = getWorker();
|
||||
if (!w) return Promise.reject(new Error('No worker'));
|
||||
if (!w) return Promise.reject(new Error(trText('No worker')));
|
||||
const id = ++messageId;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
@@ -66,10 +91,11 @@ export async function diffTextRunsAsync(
|
||||
type: 'diff',
|
||||
beforeItems,
|
||||
afterItems,
|
||||
strings: diffStrings(),
|
||||
})) as { changes: CompareTextChange[]; summary: CompareChangeSummary };
|
||||
return result;
|
||||
} catch {
|
||||
return diffTextRuns(beforeItems, afterItems);
|
||||
return diffTextRuns(beforeItems, afterItems, diffStrings());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, trHtml } from '../i18n/i18n.js';
|
||||
|
||||
export {
|
||||
getRotationState,
|
||||
updateRotationState,
|
||||
@@ -278,7 +280,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
}
|
||||
};
|
||||
|
||||
const infoSection = createSection('Info Dictionary');
|
||||
const infoSection = createSection(trText('Info Dictionary'));
|
||||
if (info && Object.keys(info).length > 0) {
|
||||
for (const key in info) {
|
||||
const value = (info as Record<string, unknown>)[key];
|
||||
@@ -310,11 +312,11 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
infoSection.ul.appendChild(createListItem(key, displayValue));
|
||||
}
|
||||
} else {
|
||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">${trHtml('- No Info Dictionary data found -')}</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(infoSection.wrapper);
|
||||
|
||||
const fieldsSection = createSection('Interactive Form Fields');
|
||||
const fieldsSection = createSection(trText('Interactive Form Fields'));
|
||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||
for (const fieldName in fieldObjects) {
|
||||
const field = fieldObjects[fieldName][0] as Record<string, unknown>;
|
||||
@@ -324,7 +326,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">${trHtml('- No interactive form fields found -')}</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||
|
||||
@@ -412,7 +414,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
}
|
||||
};
|
||||
|
||||
const xmpSection = createSection('XMP Metadata');
|
||||
const xmpSection = createSection(trText('XMP Metadata'));
|
||||
if (rawXmpString) {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
@@ -431,11 +433,11 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
}
|
||||
|
||||
if (xmpSection.ul.children.length === 0) {
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">${trHtml('- No parseable XMP properties found -')}</span></li>`;
|
||||
}
|
||||
} catch (xmlError) {
|
||||
console.error('Failed to parse XMP XML:', xmlError);
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">${trHtml('- Error parsing XMP XML. Displaying raw. -')}</span></li>`;
|
||||
const pre = document.createElement('pre');
|
||||
pre.className =
|
||||
'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||
@@ -443,7 +445,7 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
xmpSection.ul.appendChild(pre);
|
||||
}
|
||||
} else {
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
|
||||
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">${trHtml('- No XMP metadata found -')}</span></li>`;
|
||||
}
|
||||
resultsDiv.appendChild(xmpSection.wrapper);
|
||||
|
||||
@@ -495,13 +497,13 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key (e.g., Department)';
|
||||
keyInput.placeholder = trText('Key (e.g., Department)');
|
||||
keyInput.className =
|
||||
'custom-meta-key w-full sm:w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||
valueInput.placeholder = trText('Value (e.g., Marketing)');
|
||||
valueInput.className =
|
||||
'custom-meta-value w-full sm:flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||
|
||||
@@ -539,7 +541,9 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
@@ -553,7 +557,9 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
const qualityValue = document.getElementById('png-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${qualitySlider.value}x`;
|
||||
qualityValue.textContent = trText('{{value}}x', {
|
||||
value: qualitySlider.value,
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
@@ -567,7 +573,9 @@ async function handleSinglePdfUpload(toolId: string, file: File) {
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
@@ -768,7 +776,9 @@ async function handleMultiFileUpload(toolId: string) {
|
||||
const value = document.getElementById('image-pdf-quality-value');
|
||||
if (slider && value) {
|
||||
const update = () =>
|
||||
(value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
|
||||
(value.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(slider.value) * 100),
|
||||
}));
|
||||
slider.addEventListener('input', update);
|
||||
update();
|
||||
}
|
||||
@@ -782,7 +792,9 @@ async function handleMultiFileUpload(toolId: string) {
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
@@ -796,7 +808,9 @@ async function handleMultiFileUpload(toolId: string) {
|
||||
const qualityValue = document.getElementById('png-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${qualitySlider.value}x`;
|
||||
qualityValue.textContent = trText('{{value}}x', {
|
||||
value: qualitySlider.value,
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
@@ -810,7 +824,9 @@ async function handleMultiFileUpload(toolId: string) {
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
|
||||
+87
-37
@@ -1,6 +1,7 @@
|
||||
import i18next from 'i18next';
|
||||
import HttpBackend from 'i18next-http-backend';
|
||||
import { getStoredItem, setStoredItem } from '../utils/safe-storage.js';
|
||||
import { setStoredItem } from '../utils/safe-storage.js';
|
||||
import { fillPlaceholders } from './placeholders.js';
|
||||
import enCommon from '../../../public/locales/en/common.json';
|
||||
import enTools from '../../../public/locales/en/tools.json';
|
||||
|
||||
@@ -82,50 +83,21 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
return langMatch[1] as SupportedLanguage;
|
||||
}
|
||||
|
||||
const storedLang = getStoredItem('i18nextLng');
|
||||
if (
|
||||
storedLang &&
|
||||
supportedLanguages.includes(storedLang as SupportedLanguage)
|
||||
) {
|
||||
return storedLang as SupportedLanguage;
|
||||
}
|
||||
|
||||
// Check browser language preferences
|
||||
if (typeof navigator !== 'undefined' && navigator.languages) {
|
||||
for (const lang of navigator.languages) {
|
||||
if (supportedLanguages.includes(lang as SupportedLanguage)) {
|
||||
return lang as SupportedLanguage;
|
||||
}
|
||||
|
||||
const primaryLang = lang.split('-')[0];
|
||||
if (supportedLanguages.includes(primaryLang as SupportedLanguage)) {
|
||||
return primaryLang as SupportedLanguage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const envLang = import.meta.env?.VITE_DEFAULT_LANGUAGE;
|
||||
if (envLang && supportedLanguages.includes(envLang as SupportedLanguage)) {
|
||||
return envLang as SupportedLanguage;
|
||||
}
|
||||
|
||||
return 'en';
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
export const LANGUAGE_CHOICE_KEY = 'bentopdf-language';
|
||||
|
||||
export const initI18n = async (): Promise<typeof i18next> => {
|
||||
if (initialized) return i18next;
|
||||
let initPromise: Promise<typeof i18next> | null = null;
|
||||
|
||||
const runInit = async (): Promise<typeof i18next> => {
|
||||
const currentLang = getLanguageFromUrl();
|
||||
|
||||
setStoredItem('i18nextLng', currentLang);
|
||||
|
||||
await i18next.use(HttpBackend).init({
|
||||
lng: currentLang,
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: supportedLanguages as unknown as string[],
|
||||
ns: ['common', 'tools'],
|
||||
ns: currentLang === 'en' ? ['common', 'tools'] : ['common', 'tools', 'ui'],
|
||||
defaultNS: 'common',
|
||||
preload: [currentLang],
|
||||
partialBundledLanguages: true,
|
||||
@@ -133,6 +105,7 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
||||
en: {
|
||||
common: enCommon,
|
||||
tools: enTools,
|
||||
ui: {},
|
||||
},
|
||||
},
|
||||
backend: {
|
||||
@@ -144,18 +117,87 @@ export const initI18n = async (): Promise<typeof i18next> => {
|
||||
});
|
||||
|
||||
await i18next.loadNamespaces('tools');
|
||||
if (currentLang !== 'en') {
|
||||
try {
|
||||
await i18next.loadNamespaces('ui');
|
||||
} catch (error) {
|
||||
console.warn('[i18n] Could not load interface messages:', error);
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
return i18next;
|
||||
};
|
||||
|
||||
export const initI18n = (): Promise<typeof i18next> => {
|
||||
initPromise ??= runInit();
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
const domReady = (): Promise<void> =>
|
||||
document.readyState === 'loading'
|
||||
? new Promise((resolve) =>
|
||||
document.addEventListener('DOMContentLoaded', () => resolve(), {
|
||||
once: true,
|
||||
})
|
||||
)
|
||||
: Promise.resolve();
|
||||
|
||||
const I18N_READY_TIMEOUT_MS = 5000;
|
||||
|
||||
export const onPageReady = (callback: () => void | Promise<void>): void => {
|
||||
const i18nReady = Promise.race([
|
||||
initI18n().then(
|
||||
(): void => undefined,
|
||||
(): void => undefined
|
||||
),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, I18N_READY_TIMEOUT_MS)),
|
||||
]);
|
||||
const holdDrop = (event: DragEvent) => event.preventDefault();
|
||||
window.addEventListener('dragover', holdDrop);
|
||||
window.addEventListener('drop', holdDrop);
|
||||
void Promise.all([domReady(), i18nReady]).then(() => {
|
||||
window.removeEventListener('dragover', holdDrop);
|
||||
window.removeEventListener('drop', holdDrop);
|
||||
return callback();
|
||||
});
|
||||
};
|
||||
|
||||
export const t = (key: string, options?: Record<string, unknown>): string => {
|
||||
return i18next.t(key, options);
|
||||
};
|
||||
|
||||
export const hasKey = (key: string): boolean => i18next.exists(key);
|
||||
|
||||
export const trText = (
|
||||
text: string,
|
||||
params?: Record<string, string | number>
|
||||
): string => {
|
||||
const language = i18next.language;
|
||||
let template = text;
|
||||
if (i18next.isInitialized && language && language !== 'en') {
|
||||
const translated = i18next.getResource(language, 'ui', text, {
|
||||
keySeparator: false,
|
||||
});
|
||||
if (typeof translated === 'string' && translated) template = translated;
|
||||
}
|
||||
return fillPlaceholders(template, params);
|
||||
};
|
||||
|
||||
const escapeForHtml = (text: string): string =>
|
||||
text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
export const trHtml = (
|
||||
text: string,
|
||||
params?: Record<string, string | number>
|
||||
): string => escapeForHtml(trText(text, params));
|
||||
|
||||
export const changeLanguage = (lang: SupportedLanguage): void => {
|
||||
if (!supportedLanguages.includes(lang)) return;
|
||||
setStoredItem('i18nextLng', lang);
|
||||
setStoredItem(LANGUAGE_CHOICE_KEY, lang);
|
||||
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
let relativePath = window.location.pathname;
|
||||
@@ -269,8 +311,16 @@ export const rewriteLinks = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathOnly =
|
||||
basePath && basePath !== '/' && href.startsWith(`${basePath}/`)
|
||||
? href.slice(basePath.length)
|
||||
: href;
|
||||
if (/^\/(blog|docs)(\/|$|[?#])/.test(pathOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const langPrefixRegex = new RegExp(
|
||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca|fa)(/|$)`
|
||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk|ca|fa)(/|$|[?#])`
|
||||
);
|
||||
if (langPrefixRegex.test(href)) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export {
|
||||
initI18n,
|
||||
onPageReady,
|
||||
t,
|
||||
trText,
|
||||
trHtml,
|
||||
changeLanguage,
|
||||
applyTranslations,
|
||||
rewriteLinks,
|
||||
|
||||
@@ -51,10 +51,16 @@ export const createLanguageSwitcher = (): HTMLElement => {
|
||||
searchWrapper.className =
|
||||
'p-2 border-b border-gray-700 bg-gray-800 flex-shrink-0';
|
||||
|
||||
const searchPlaceholder =
|
||||
t('nav.searchLanguage') !== 'nav.searchLanguage'
|
||||
? t('nav.searchLanguage')
|
||||
: 'Search language…';
|
||||
const translated = (key: string, fallback: string): string => {
|
||||
const value = t(key);
|
||||
return typeof value === 'string' && value && value !== key
|
||||
? value
|
||||
: fallback;
|
||||
};
|
||||
const searchPlaceholder = translated(
|
||||
'nav.searchLanguage',
|
||||
'Search language…'
|
||||
);
|
||||
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'search';
|
||||
@@ -76,10 +82,7 @@ export const createLanguageSwitcher = (): HTMLElement => {
|
||||
|
||||
const emptyState = document.createElement('p');
|
||||
emptyState.className = 'hidden px-4 py-3 text-sm text-gray-400 text-center';
|
||||
const emptyText =
|
||||
t('nav.noLanguagesFound') !== 'nav.noLanguagesFound'
|
||||
? t('nav.noLanguagesFound')
|
||||
: 'No languages found';
|
||||
const emptyText = translated('nav.noLanguagesFound', 'No languages found');
|
||||
emptyState.textContent = emptyText;
|
||||
|
||||
const options: HTMLButtonElement[] = [];
|
||||
@@ -176,18 +179,9 @@ export const injectLanguageSwitcher = (): void => {
|
||||
const footer = document.querySelector('footer');
|
||||
if (!footer) return;
|
||||
|
||||
const headings = footer.querySelectorAll('h3');
|
||||
let followUsColumn: HTMLElement | null = null;
|
||||
|
||||
headings.forEach((h3) => {
|
||||
if (
|
||||
h3.textContent?.trim() === 'Follow Us' ||
|
||||
h3.textContent?.trim() === 'Folgen Sie uns' ||
|
||||
h3.textContent?.trim() === 'Theo dõi chúng tôi'
|
||||
) {
|
||||
followUsColumn = h3.parentElement;
|
||||
}
|
||||
});
|
||||
const followUsHeading = footer.querySelector('[data-i18n="footer.followUs"]');
|
||||
const followUsColumn: HTMLElement | null =
|
||||
followUsHeading?.parentElement ?? null;
|
||||
|
||||
if (followUsColumn) {
|
||||
const socialIconsContainer = followUsColumn.querySelector('.space-x-4');
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const fillPlaceholders = (
|
||||
text: string,
|
||||
params?: Record<string, string | number>
|
||||
): string => {
|
||||
if (!params) return text;
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (match, name: string) =>
|
||||
Object.prototype.hasOwnProperty.call(params, name)
|
||||
? String(params[name])
|
||||
: match
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
(function () {
|
||||
var script = document.currentScript;
|
||||
if (!script) return;
|
||||
var base = (script.getAttribute('data-base') || '/').replace(/\/?$/, '/');
|
||||
var languages = (script.getAttribute('data-languages') || '').split(',');
|
||||
var fallback = script.getAttribute('data-default-language') || 'en';
|
||||
var path = window.location.pathname;
|
||||
if (path + '/' === base) path = base;
|
||||
if (path.indexOf(base) !== 0) return;
|
||||
var rest = path.slice(base.length);
|
||||
var first = rest.split('/')[0];
|
||||
if (first === 'en' || languages.indexOf(first) !== -1) return;
|
||||
if (/^(blog|docs)(\/|$)/.test(rest)) return;
|
||||
|
||||
var choice;
|
||||
try {
|
||||
choice = window.localStorage.getItem('bentopdf-language');
|
||||
} catch {
|
||||
choice = null;
|
||||
}
|
||||
|
||||
function supported(code) {
|
||||
var lower = code.toLowerCase();
|
||||
if (/^zh-(tw|hk|mo|hant)/.test(lower)) return 'zh-TW';
|
||||
var primary = lower.split('-')[0];
|
||||
if (primary === 'en') return 'en';
|
||||
return languages.indexOf(primary) !== -1 ? primary : null;
|
||||
}
|
||||
|
||||
function cameFromThisSite() {
|
||||
var origin = window.location.origin + '/';
|
||||
return document.referrer.slice(0, origin.length) === origin;
|
||||
}
|
||||
|
||||
function detect() {
|
||||
var preferred = window.navigator.languages || [window.navigator.language];
|
||||
for (var i = 0; i < preferred.length; i++) {
|
||||
if (!preferred[i]) continue;
|
||||
var match = supported(preferred[i]);
|
||||
if (match) return match;
|
||||
}
|
||||
return languages.indexOf(fallback) !== -1 ? fallback : 'en';
|
||||
}
|
||||
|
||||
var target = null;
|
||||
if (choice) {
|
||||
if (languages.indexOf(choice) !== -1) target = choice;
|
||||
} else if ((rest === '' || rest === 'index.html') && !cameFromThisSite()) {
|
||||
target = detect();
|
||||
}
|
||||
if (!target || target === 'en') return;
|
||||
|
||||
var page = rest === 'index.html' ? '' : rest;
|
||||
window.location.replace(
|
||||
base +
|
||||
target +
|
||||
(page ? '/' + page : '') +
|
||||
window.location.search +
|
||||
window.location.hash
|
||||
);
|
||||
})();
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const worker = new Worker(
|
||||
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
|
||||
);
|
||||
@@ -72,7 +74,9 @@ worker.onmessage = function (e) {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`${pageState.attachments.length} file(s) attached successfully.`,
|
||||
trText('{{length}} file(s) attached successfully.', {
|
||||
length: pageState.attachments.length,
|
||||
}),
|
||||
'success',
|
||||
function () {
|
||||
resetState();
|
||||
@@ -112,7 +116,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -140,7 +146,10 @@ async function updateUI() {
|
||||
pageState.pdfDoc = await loadPdfDocument(result.bytes);
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
||||
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
|
||||
@@ -249,7 +258,11 @@ async function addAttachments() {
|
||||
for (let i = 0; i < pageState.attachments.length; i++) {
|
||||
const file = pageState.attachments[i];
|
||||
showLoader(
|
||||
`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`
|
||||
trText('Reading {{name}} ({{n}}/{{length}})...', {
|
||||
name: file.name,
|
||||
n: i + 1,
|
||||
length: pageState.attachments.length,
|
||||
})
|
||||
);
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
@@ -276,7 +289,9 @@ async function addAttachments() {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`Failed to attach files: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
trText('Failed to attach files: {{error}}', {
|
||||
error: error instanceof Error ? error.message : trText('Unknown error'),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -301,7 +316,7 @@ function handleAttachmentSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const attachmentInput = document.getElementById(
|
||||
|
||||
@@ -6,6 +6,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { AddBlankPageState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: AddBlankPageState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -61,7 +63,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -90,10 +94,16 @@ async function updateUI() {
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
if (pagePositionHint) {
|
||||
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
|
||||
pagePositionHint.textContent = trText(
|
||||
'Enter 0 to insert at the beginning, or {{pageCount}} to insert at the end.',
|
||||
{ pageCount }
|
||||
);
|
||||
}
|
||||
if (pagePositionInput) {
|
||||
pagePositionInput.max = pageCount.toString();
|
||||
@@ -131,7 +141,9 @@ async function addBlankPages() {
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert(
|
||||
'Invalid Input',
|
||||
`Please enter a number between 0 and ${totalPages}.`
|
||||
trText('Please enter a number between 0 and {{totalPages}}.', {
|
||||
totalPages,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +157,9 @@ async function addBlankPages() {
|
||||
}
|
||||
|
||||
showLoader(
|
||||
`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`
|
||||
insertCount > 1
|
||||
? trText('Adding {{insertCount}} blank pages...', { insertCount })
|
||||
: trText('Adding {{insertCount}} blank page...', { insertCount })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -185,7 +199,13 @@ async function addBlankPages() {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`,
|
||||
insertCount > 1
|
||||
? trText('Added {{insertCount}} blank pages successfully!', {
|
||||
insertCount,
|
||||
})
|
||||
: trText('Added {{insertCount}} blank page successfully!', {
|
||||
insertCount,
|
||||
}),
|
||||
'success',
|
||||
function () {
|
||||
resetState();
|
||||
@@ -195,7 +215,9 @@ async function addBlankPages() {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`Could not add blank page${insertCount > 1 ? 's' : ''}.`
|
||||
insertCount > 1
|
||||
? trText('Could not add blank pages.')
|
||||
: trText('Could not add blank page.')
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
@@ -215,7 +237,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
CpdfInstance,
|
||||
} from '@/types';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, onPageReady } from '../i18n/index.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
@@ -47,11 +47,7 @@ const pageState: AddPageLabelsState = {
|
||||
rules: [createLabelRule()],
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function createLabelRule(overrides: Partial<LabelRule> = {}): LabelRule {
|
||||
labelRuleCounter += 1;
|
||||
|
||||
@@ -7,6 +7,9 @@ import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
import { getStoredItem, setStoredItem } from '../utils/safe-storage.js';
|
||||
|
||||
let selectedFile: File | null = null;
|
||||
let viewerIframe: HTMLIFrameElement | null = null;
|
||||
let currentBlobUrl: string | null = null;
|
||||
@@ -55,7 +58,7 @@ function resetState() {
|
||||
viewerContainer.style.aspectRatio = '';
|
||||
}
|
||||
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||
const isFullWidth = getStoredItem('fullWidthMode') !== 'false';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-6xl');
|
||||
toolUploader.classList.add('max-w-2xl');
|
||||
@@ -76,7 +79,7 @@ function updateFileList() {
|
||||
fileListDiv.innerHTML = '';
|
||||
|
||||
// Expand container width for viewer if NOT in full width mode (default to true if not set)
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||
const isFullWidth = getStoredItem('fullWidthMode') !== 'false';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-2xl');
|
||||
toolUploader.classList.add('max-w-6xl');
|
||||
@@ -105,7 +108,7 @@ function updateFileList() {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className =
|
||||
'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2';
|
||||
deleteBtn.title = 'Remove file';
|
||||
deleteBtn.title = trText('Remove file');
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -165,14 +168,14 @@ async function loadPdfInViewer(file: File) {
|
||||
currentBlobUrl = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
|
||||
const existingPrefsRaw = getStoredItem('pdfjs.preferences');
|
||||
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
|
||||
delete (existingPrefs as Record<string, unknown>).annotationEditorMode;
|
||||
const newPrefs = {
|
||||
...existingPrefs,
|
||||
enablePermissions: false,
|
||||
};
|
||||
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||
setStoredItem('pdfjs.preferences', JSON.stringify(newPrefs));
|
||||
} catch (e) {
|
||||
console.warn('Failed to update pdfjs.preferences in localStorage', e);
|
||||
}
|
||||
@@ -301,7 +304,9 @@ if (saveStampedBtn) {
|
||||
saveStampedBtn.addEventListener('click', () => {
|
||||
if (!viewerIframe) {
|
||||
alert(
|
||||
'Viewer not ready. Please upload a PDF and wait for it to finish loading.'
|
||||
trText(
|
||||
'Viewer not ready. Please upload a PDF and wait for it to finish loading.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -338,12 +343,16 @@ if (saveStampedBtn) {
|
||||
}
|
||||
|
||||
alert(
|
||||
'Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||
trText(
|
||||
'Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger stamped PDF export:', e);
|
||||
alert(
|
||||
'Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||
trText(
|
||||
'Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: AddWatermarkState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -58,11 +60,7 @@ let cachedPdfjsDoc: pdfjsLib.PDFDocumentProxy | null = null;
|
||||
const pageWatermarks: Map<number, PageWatermarkConfig> = new Map();
|
||||
let applyToAllPages = true;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -145,7 +143,10 @@ async function handleFiles(files: FileList) {
|
||||
|
||||
const editorFileInfo = document.getElementById('editor-file-info');
|
||||
if (editorFileInfo) {
|
||||
editorFileInfo.textContent = `${file.name} (${formatBytes(file.size)}, ${totalPageCount} pages)`;
|
||||
editorFileInfo.textContent = trText(
|
||||
'{{name}} ({{size}}, {{totalPageCount}} pages)',
|
||||
{ name: file.name, size: formatBytes(file.size), totalPageCount }
|
||||
);
|
||||
}
|
||||
|
||||
updatePageNavUI();
|
||||
@@ -173,7 +174,10 @@ function updateFileDisplay() {
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{getPageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
getPageCount: pageState.pdfDoc.getPageCount(),
|
||||
});
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
@@ -1068,7 +1072,7 @@ async function applyWatermark() {
|
||||
if (config.type === 'text') {
|
||||
const text = config.text;
|
||||
if (!text.trim())
|
||||
throw new Error('Please enter text for the watermark.');
|
||||
throw new Error(trText('Please enter text for the watermark.'));
|
||||
const textColor = hexToRgb(config.color);
|
||||
|
||||
resultBytes = new Uint8Array(
|
||||
@@ -1089,7 +1093,9 @@ async function applyWatermark() {
|
||||
} else {
|
||||
const imageFile = config.imageFile;
|
||||
if (!imageFile)
|
||||
throw new Error('Please select an image file for the watermark.');
|
||||
throw new Error(
|
||||
trText('Please select an image file for the watermark.')
|
||||
);
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
|
||||
let imageType: 'png' | 'jpg';
|
||||
@@ -1099,7 +1105,9 @@ async function applyWatermark() {
|
||||
imageType = 'jpg';
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||
trText(
|
||||
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1223,7 +1231,12 @@ async function applyWatermark() {
|
||||
const renderScale = 2.5;
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
showLoader(`Flattening page ${i} of ${totalPages}...`);
|
||||
showLoader(
|
||||
trText('Flattening page {{i}} of {{totalPages}}...', {
|
||||
i,
|
||||
totalPages,
|
||||
})
|
||||
);
|
||||
const page = await watermarkedPdf.getPage(i);
|
||||
const unscaledVP = page.getViewport({ scale: 1 });
|
||||
const viewport = page.getViewport({ scale: renderScale });
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
import { applyColorAdjustments } from '../utils/image-effects.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import type { AdjustColorsSettings } from '../types/adjust-colors-type.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -121,7 +121,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -144,7 +147,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf: pdfjsLib.PDFDocumentProxy) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
@@ -181,7 +188,12 @@ async function processAllPages(): Promise<void> {
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
for (let i = 1; i <= doc.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${doc.numPages}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{i}} of {{numPages}}...', {
|
||||
i,
|
||||
numPages: doc.numPages,
|
||||
})
|
||||
);
|
||||
|
||||
const page = await doc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
@@ -331,7 +343,7 @@ function setupSettingsListeners(): void {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -9,6 +9,8 @@ import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { interleavePdfs } from '../utils/alternate-merge.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: AlternateMergeState = {
|
||||
files: [],
|
||||
pdfBytes: new Map(),
|
||||
@@ -50,7 +52,9 @@ async function updateUI() {
|
||||
|
||||
const infoSpan = document.createElement('span');
|
||||
infoSpan.className = 'text-gray-200';
|
||||
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
|
||||
infoSpan.textContent = trText('{{length}} PDF files selected', {
|
||||
length: pageState.files.length,
|
||||
});
|
||||
|
||||
const clearBtn = document.createElement('button');
|
||||
clearBtn.className = 'text-red-400 hover:text-red-300';
|
||||
@@ -94,7 +98,10 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('span');
|
||||
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
infoDiv.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -147,7 +154,7 @@ async function mixPages() {
|
||||
|
||||
try {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
if (!fileList) throw new Error(trText('File list not found'));
|
||||
|
||||
const sortedFileNames = Array.from(fileList.children)
|
||||
.map(function (li) {
|
||||
@@ -199,7 +206,7 @@ async function mixPages() {
|
||||
const message =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: 'An error occurred while mixing the PDFs.';
|
||||
: trText('An error occurred while mixing the PDFs.');
|
||||
showAlert('Error', message);
|
||||
}
|
||||
}
|
||||
@@ -218,7 +225,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -6,13 +6,11 @@ import { BackgroundColorState } from '@/types';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: BackgroundColorState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -90,7 +88,10 @@ function updateFileDisplay() {
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{getPageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
getPageCount: pageState.pdfDoc.getPageCount(),
|
||||
});
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
|
||||
@@ -8,6 +8,8 @@ import Sortable from 'sortablejs';
|
||||
import { FileEntry, Position, StylePreset } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const FONT_MAP: Record<string, keyof typeof StandardFonts> = {
|
||||
Helvetica: 'Helvetica',
|
||||
TimesRoman: 'TimesRoman',
|
||||
@@ -54,11 +56,7 @@ const STYLE_PRESETS: Record<string, StylePreset> = {
|
||||
|
||||
const files: FileEntry[] = [];
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -237,7 +235,10 @@ function renderFileList() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(entry.file.size)} \u2022 ${entry.pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(entry.file.size),
|
||||
pageCount: entry.pageCount,
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
leftSection.append(dragHandle, infoContainer);
|
||||
@@ -260,7 +261,16 @@ function renderFileList() {
|
||||
|
||||
const summary = document.createElement('div');
|
||||
summary.className = 'text-xs text-gray-400 mt-1';
|
||||
summary.textContent = `${files.length} file${files.length !== 1 ? 's' : ''} \u2022 ${totalPages} total pages`;
|
||||
summary.textContent =
|
||||
files.length !== 1
|
||||
? trText('{{length}} files • {{totalPages}} total pages', {
|
||||
length: files.length,
|
||||
totalPages,
|
||||
})
|
||||
: trText('{{length}} file • {{totalPages}} total pages', {
|
||||
length: files.length,
|
||||
totalPages,
|
||||
});
|
||||
fileListEl.appendChild(summary);
|
||||
}
|
||||
|
||||
@@ -539,7 +549,10 @@ async function applyBatesNumbers() {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`Bates numbers applied successfully! (${batesStart} through ${batesCounter - 1})`,
|
||||
trText(
|
||||
'Bates numbers applied successfully! ({{batesStart}} through {{n}})',
|
||||
{ batesStart, n: batesCounter - 1 }
|
||||
),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
|
||||
+175
-165
@@ -3,202 +3,212 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
async function convertImageToPngBytes(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise<Blob | null>((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
if (!pngBlob) {
|
||||
reject(new Error('Failed to create PNG blob'));
|
||||
return;
|
||||
}
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error(trText('Failed to get canvas context')));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise<Blob | null>((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
if (!pngBlob) {
|
||||
reject(new Error(trText('Failed to create PNG blob')));
|
||||
return;
|
||||
}
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error(trText('Failed to load image.')));
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.onerror = () => reject(new Error(trText('Failed to read file.')));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_bmps.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_bmps.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) =>
|
||||
file.type === 'image/bmp' || file.name.toLowerCase().endsWith('.bmp')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert(
|
||||
'Invalid Files',
|
||||
'Some files were skipped. Only BMP files are allowed.'
|
||||
);
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'image/bmp' || file.name.toLowerCase().endsWith('.bmp')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only BMP files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
+101
-70
@@ -12,6 +12,7 @@ import {
|
||||
escapeHtml,
|
||||
hexToRgb,
|
||||
} from '../utils/helpers.js';
|
||||
import { trText, trHtml, onPageReady } from '../i18n/i18n.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
@@ -262,21 +263,21 @@ placeholder="${escapeHTML(field.placeholder || '')}" />
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="flex items-center gap-1 text-xs">
|
||||
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
|
||||
<span class="text-gray-700">Set custom destination</span>
|
||||
<span class="text-gray-700">${trHtml('Set custom destination')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">Page</label>
|
||||
<label class="text-xs text-gray-600">${trHtml('Page')}</label>
|
||||
<input type="number" id="modal-dest-page" min="1" max="${escapeHTML(String(field.maxPages || 1))}" value="${escapeHTML(String(defaultValues.destPage || field.page || 1))}"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" step="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">Zoom(%)</label>
|
||||
<label class="text-xs text-gray-600">${trHtml('Zoom(%)')}</label>
|
||||
<select id="modal-dest-zoom" class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900">
|
||||
<option value="">Inherit</option>
|
||||
<option value="0">Fit Page</option>
|
||||
<option value="">${trHtml('Inherit')}</option>
|
||||
<option value="0">${trHtml('Fit Page')}</option>
|
||||
<option value="50">50%</option>
|
||||
<option value="75">75%</option>
|
||||
<option value="100">100%</option>
|
||||
@@ -288,20 +289,20 @@ class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" st
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">X Position</label>
|
||||
<label class="text-xs text-gray-600">${trHtml('X Position')}</label>
|
||||
<input type="number" id="modal-dest-x" value="0" step="10"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">Y Position</label>
|
||||
<label class="text-xs text-gray-600">${trHtml('Y Position')}</label>
|
||||
<input type="number" id="modal-dest-y" value="0" step="10"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" />
|
||||
</div>
|
||||
</div>
|
||||
<button id="modal-pick-destination" class="w-full px-3 py-2 btn-gradient text-white rounded text-xs !flex items-center justify-center gap-1">
|
||||
<i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location
|
||||
<i data-lucide="crosshair" class="w-3 h-3"></i> ${trHtml('Click on PDF to Pick Location')}
|
||||
</button>
|
||||
<p class="text-xs text-gray-500 italic">Click the button above, then click on the PDF where you want the bookmark to jump to</p>
|
||||
<p class="text-xs text-gray-500 italic">${trHtml('Click the button above, then click on the PDF where you want the bookmark to jump to')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,7 +312,7 @@ class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" />
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">${escapeHTML(field.label)}</label>
|
||||
<div id="modal-preview" class="style-preview bg-gray-50">
|
||||
<span id="preview-text" style="font-size: 16px;">Preview Text</span>
|
||||
<span id="preview-text" style="font-size: 16px;">${trHtml('Preview Text')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -322,13 +323,13 @@ class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" />
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${escapeHTML(title)}</h3>
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${escapeHTML(trText(title))}</h3>
|
||||
<div class="mb-6">
|
||||
${fieldsHTML}
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-gray-700">Cancel</button>
|
||||
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
|
||||
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-gray-700">${trHtml('Cancel')}</button>
|
||||
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">${trHtml('Confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -354,11 +355,13 @@ class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" />
|
||||
'#modal-color-picker'
|
||||
) as HTMLInputElement | null;
|
||||
|
||||
const titleVal = titleInputEl ? titleInputEl.value : 'Preview Text';
|
||||
const titleVal = titleInputEl
|
||||
? titleInputEl.value
|
||||
: trText('Preview Text');
|
||||
const color = colorSelectEl ? colorSelectEl.value : '';
|
||||
const style = styleSelectEl ? styleSelectEl.value : '';
|
||||
|
||||
previewText.textContent = titleVal || 'Preview Text';
|
||||
previewText.textContent = titleVal || trText('Preview Text');
|
||||
|
||||
if (color === 'custom' && colorPickerEl) {
|
||||
previewText.style.color = colorPickerEl.value;
|
||||
@@ -695,7 +698,7 @@ function cancelDestinationPicking(): void {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
initializeGlobalShortcuts();
|
||||
|
||||
const canvasEl = document.getElementById(
|
||||
@@ -723,7 +726,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
canvasWrapperEl.appendChild(coordTooltip);
|
||||
}
|
||||
|
||||
coordTooltip.textContent = `X: ${Math.round(x)}, Y: ${Math.round(y)} `;
|
||||
coordTooltip.textContent = trText('X: {{x}}, Y: {{y}} ', {
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
});
|
||||
coordTooltip.style.left = e.clientX - rect.left + 15 + 'px';
|
||||
coordTooltip.style.top = e.clientY - rect.top + 15 + 'px';
|
||||
});
|
||||
@@ -797,7 +803,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
canvasX + canvasRect.left - wrapperRect.left + 20 + 'px';
|
||||
coordDisplay.style.top =
|
||||
canvasY + canvasRect.top - wrapperRect.top - 30 + 'px';
|
||||
coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)} `;
|
||||
coordDisplay.textContent = trText('X: {{pdfX}}, Y: {{pdfY}} ', {
|
||||
pdfX: Math.round(pdfX),
|
||||
pdfY: Math.round(pdfY),
|
||||
});
|
||||
canvasWrapperEl.appendChild(coordDisplay);
|
||||
|
||||
currentPickingCallback(currentPage, pdfX, pdfY);
|
||||
@@ -825,11 +834,11 @@ function showConfirmModal(message: string): Promise<boolean> {
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
|
||||
<p class="text-gray-600 mb-6">${escapeHTML(message)}</p>
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${trHtml('Confirm Action')}</h3>
|
||||
<p class="text-gray-600 mb-6">${escapeHTML(trText(message))}</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-gray-700">Cancel</button>
|
||||
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
|
||||
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-gray-700">${trHtml('Cancel')}</button>
|
||||
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">${trHtml('Confirm')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -881,7 +890,7 @@ function showAlertModal(title: string, message: string): Promise<boolean> {
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${escapeHTML(title)}</h3>
|
||||
<p class="text-gray-600 mb-6">${escapeHTML(message)}</p>
|
||||
<div class="flex justify-end">
|
||||
<button id="modal-ok" class="px-4 py-2 rounded btn-gradient text-white">OK</button>
|
||||
<button id="modal-ok" class="px-4 py-2 rounded btn-gradient text-white">${trHtml('OK')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1005,7 +1014,9 @@ redoBtn?.addEventListener('click', redo);
|
||||
|
||||
resetBtn?.addEventListener('click', async () => {
|
||||
const confirmed = await showConfirmModal(
|
||||
'Reset and go back to file uploader? All unsaved changes will be lost.'
|
||||
trText(
|
||||
'Reset and go back to file uploader? All unsaved changes will be lost.'
|
||||
)
|
||||
);
|
||||
if (confirmed) {
|
||||
resetToUploader();
|
||||
@@ -1014,12 +1025,14 @@ resetBtn?.addEventListener('click', async () => {
|
||||
|
||||
deleteAllBtn?.addEventListener('click', async () => {
|
||||
if (bookmarkTree.length === 0) {
|
||||
await showAlertModal('Info', 'No bookmarks to delete.');
|
||||
await showAlertModal('Info', trText('No bookmarks to delete.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await showConfirmModal(
|
||||
`Delete all ${bookmarkTree.length} bookmark(s) ? `
|
||||
trText('Delete all {{length}} bookmark(s) ? ', {
|
||||
length: bookmarkTree.length,
|
||||
})
|
||||
);
|
||||
if (confirmed) {
|
||||
bookmarkTree = [];
|
||||
@@ -1146,7 +1159,7 @@ batchDeleteBtn?.addEventListener('click', async () => {
|
||||
if (selectedBookmarks.size === 0) return;
|
||||
|
||||
const confirmed = await showConfirmModal(
|
||||
`Delete ${selectedBookmarks.size} bookmark(s) ? `
|
||||
trText('Delete {{size}} bookmark(s) ? ', { size: selectedBookmarks.size })
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1292,8 +1305,10 @@ csvInput?.addEventListener('change', async (e: Event) => {
|
||||
csvBookmarks = parseCSV(text);
|
||||
|
||||
await showAlertModal(
|
||||
'CSV Loaded',
|
||||
`Loaded ${csvBookmarks.length} bookmarks from CSV. Now upload your PDF.`
|
||||
trText('CSV Loaded'),
|
||||
trText('Loaded {{length}} bookmarks from CSV. Now upload your PDF.', {
|
||||
length: csvBookmarks.length,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1306,11 +1321,11 @@ jsonInput?.addEventListener('change', async (e: Event) => {
|
||||
try {
|
||||
jsonBookmarks = JSON.parse(text);
|
||||
await showAlertModal(
|
||||
'JSON Loaded',
|
||||
'Loaded bookmarks from JSON. Now upload your PDF.'
|
||||
trText('JSON Loaded'),
|
||||
trText('Loaded bookmarks from JSON. Now upload your PDF.')
|
||||
);
|
||||
} catch {
|
||||
await showAlertModal('Error', 'Invalid JSON format');
|
||||
await showAlertModal('Error', trText('Invalid JSON format'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1388,7 +1403,10 @@ async function renderPage(
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
pageIndicator!.textContent = `Page ${num} / ${pdfJsDoc.numPages}`;
|
||||
pageIndicator!.textContent = trText('Page {{num}} / {{numPages}}', {
|
||||
num,
|
||||
numPages: pdfJsDoc.numPages,
|
||||
});
|
||||
if (gotoPageInput) gotoPageInput.value = String(num);
|
||||
currentPage = num;
|
||||
if (currentPageDisplay) currentPageDisplay.textContent = String(num);
|
||||
@@ -1425,7 +1443,9 @@ gotoPageInput?.addEventListener('keypress', (e: KeyboardEvent) => {
|
||||
|
||||
function updateZoomIndicator(): void {
|
||||
if (zoomIndicator) {
|
||||
zoomIndicator.textContent = `${Math.round(currentZoom * 100)}%`;
|
||||
zoomIndicator.textContent = trText('{{value}}%', {
|
||||
value: Math.round(currentZoom * 100),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,7 +1719,7 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
|
||||
|
||||
titleDiv.innerHTML = `
|
||||
<span class="text-sm block ${styleClass} ${textColorClass}" ${customColorStyle}>${escapeHTML(node.title)}${destinationIcon}</span>
|
||||
<span class="text-xs text-gray-500">Page ${escapeHTML(String(node.page))}</span>
|
||||
<span class="text-xs text-gray-500">${trHtml('Page {{page}}', { page: escapeHTML(String(node.page)) })}</span>
|
||||
`;
|
||||
|
||||
titleDiv.addEventListener('click', async () => {
|
||||
@@ -1734,16 +1754,16 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
|
||||
|
||||
const addChildBtn = document.createElement('button');
|
||||
addChildBtn.className = 'p-1 hover:bg-gray-200 rounded text-gray-700';
|
||||
addChildBtn.title = 'Add child';
|
||||
addChildBtn.title = trText('Add child');
|
||||
addChildBtn.innerHTML = '<i data-lucide="plus" class="w-4 h-4"></i>';
|
||||
addChildBtn.addEventListener('click', async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const result = await showInputModal('Add Child Bookmark', [
|
||||
const result = await showInputModal(trText('Add Child Bookmark'), [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
placeholder: 'Enter bookmark title',
|
||||
label: trText('Title'),
|
||||
placeholder: trText('Enter bookmark title'),
|
||||
},
|
||||
]);
|
||||
if (result && result.title) {
|
||||
@@ -1767,52 +1787,52 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'p-1 hover:bg-gray-200 rounded text-gray-700';
|
||||
editBtn.title = 'Edit';
|
||||
editBtn.title = trText('Edit');
|
||||
editBtn.innerHTML = '<i data-lucide="edit-2" class="w-4 h-4"></i>';
|
||||
editBtn.addEventListener('click', async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const result = await showInputModal(
|
||||
'Edit Bookmark',
|
||||
trText('Edit Bookmark'),
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
placeholder: 'Enter bookmark title',
|
||||
label: trText('Title'),
|
||||
placeholder: trText('Enter bookmark title'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'color',
|
||||
label: 'Color',
|
||||
label: trText('Color'),
|
||||
options: [
|
||||
{ value: '', label: 'None' },
|
||||
{ value: 'red', label: 'Red' },
|
||||
{ value: 'blue', label: 'Blue' },
|
||||
{ value: 'green', label: 'Green' },
|
||||
{ value: 'yellow', label: 'Yellow' },
|
||||
{ value: 'purple', label: 'Purple' },
|
||||
{ value: 'custom', label: 'Custom...' },
|
||||
{ value: '', label: trText('None') },
|
||||
{ value: 'red', label: trText('Red') },
|
||||
{ value: 'blue', label: trText('Blue') },
|
||||
{ value: 'green', label: trText('Green') },
|
||||
{ value: 'yellow', label: trText('Yellow') },
|
||||
{ value: 'purple', label: trText('Purple') },
|
||||
{ value: 'custom', label: trText('Custom...') },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'style',
|
||||
label: 'Style',
|
||||
label: trText('Style'),
|
||||
options: [
|
||||
{ value: '', label: 'Normal' },
|
||||
{ value: 'bold', label: 'Bold' },
|
||||
{ value: 'italic', label: 'Italic' },
|
||||
{ value: 'bold-italic', label: 'Bold & Italic' },
|
||||
{ value: '', label: trText('Normal') },
|
||||
{ value: 'bold', label: trText('Bold') },
|
||||
{ value: 'italic', label: trText('Italic') },
|
||||
{ value: 'bold-italic', label: trText('Bold & Italic') },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'destination',
|
||||
name: 'destination',
|
||||
label: 'Destination',
|
||||
label: trText('Destination'),
|
||||
page: node.page,
|
||||
maxPages: pdfJsDoc ? pdfJsDoc.numPages : 1,
|
||||
},
|
||||
{ type: 'preview', label: 'Preview' },
|
||||
{ type: 'preview', label: trText('Preview') },
|
||||
],
|
||||
{
|
||||
title: node.title,
|
||||
@@ -1845,11 +1865,13 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'p-1 hover:bg-gray-200 rounded text-red-600';
|
||||
deleteBtn.title = 'Delete';
|
||||
deleteBtn.title = trText('Delete');
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
deleteBtn.addEventListener('click', async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const confirmed = await showConfirmModal(`Delete "${node.title}"?`);
|
||||
const confirmed = await showConfirmModal(
|
||||
trText('Delete "{{title}}"?', { title: node.title })
|
||||
);
|
||||
if (confirmed) {
|
||||
removeNodeById(bookmarkTree, node.id);
|
||||
saveState();
|
||||
@@ -1883,7 +1905,7 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
|
||||
addTopLevelBtn?.addEventListener('click', async () => {
|
||||
const title = titleInput?.value.trim();
|
||||
if (!title) {
|
||||
await showAlertModal('Error', 'Please enter a title.');
|
||||
await showAlertModal('Error', trText('Please enter a title.'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1929,7 +1951,10 @@ csvImportHidden?.addEventListener('change', async (e: Event) => {
|
||||
bookmarkTree = imported;
|
||||
saveState();
|
||||
renderBookmarkTree();
|
||||
await showAlertModal('Success', `Imported ${imported.length} bookmarks!`);
|
||||
await showAlertModal(
|
||||
'Success',
|
||||
trText('Imported {{length}} bookmarks!', { length: imported.length })
|
||||
);
|
||||
}
|
||||
|
||||
if (csvImportHidden) csvImportHidden.value = '';
|
||||
@@ -1939,7 +1964,7 @@ exportCsvBtn?.addEventListener('click', () => {
|
||||
exportDropdown?.classList.add('hidden');
|
||||
|
||||
if (bookmarkTree.length === 0) {
|
||||
showAlertModal('Error', 'No bookmarks to export!');
|
||||
showAlertModal('Error', trText('No bookmarks to export!'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2045,9 +2070,9 @@ jsonImportHidden?.addEventListener('change', async (e: Event) => {
|
||||
bookmarkTree = imported;
|
||||
saveState();
|
||||
renderBookmarkTree();
|
||||
await showAlertModal('Success', 'Bookmarks imported from JSON!');
|
||||
await showAlertModal('Success', trText('Bookmarks imported from JSON!'));
|
||||
} catch {
|
||||
await showAlertModal('Error', 'Invalid JSON format');
|
||||
await showAlertModal('Error', trText('Invalid JSON format'));
|
||||
}
|
||||
|
||||
if (jsonImportHidden) jsonImportHidden.value = '';
|
||||
@@ -2057,7 +2082,7 @@ exportJsonBtn?.addEventListener('click', () => {
|
||||
exportDropdown?.classList.add('hidden');
|
||||
|
||||
if (bookmarkTree.length === 0) {
|
||||
showAlertModal('Error', 'No bookmarks to export!');
|
||||
showAlertModal('Error', trText('No bookmarks to export!'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2072,7 +2097,10 @@ extractExistingBtn?.addEventListener('click', async () => {
|
||||
const extracted = await extractExistingBookmarks();
|
||||
if (extracted.length > 0) {
|
||||
const confirmed = await showConfirmModal(
|
||||
`Found ${extracted.length} existing bookmarks. Replace current bookmarks?`
|
||||
trText(
|
||||
'Found {{length}} existing bookmarks. Replace current bookmarks?',
|
||||
{ length: extracted.length }
|
||||
)
|
||||
);
|
||||
if (confirmed) {
|
||||
bookmarkTree = extracted;
|
||||
@@ -2080,7 +2108,10 @@ extractExistingBtn?.addEventListener('click', async () => {
|
||||
renderBookmarkTree();
|
||||
}
|
||||
} else {
|
||||
await showAlertModal('Info', 'No existing bookmarks found in this PDF.');
|
||||
await showAlertModal(
|
||||
'Info',
|
||||
trText('No existing bookmarks found in this PDF.')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2340,7 +2371,7 @@ downloadBtn?.addEventListener('click', async () => {
|
||||
});
|
||||
downloadFile(blob, `${originalFileName}.pdf`);
|
||||
|
||||
await showAlertModal('Success', 'PDF saved successfully!');
|
||||
await showAlertModal('Success', trText('PDF saved successfully!'));
|
||||
|
||||
setTimeout(() => {
|
||||
resetToUploader();
|
||||
@@ -2349,7 +2380,7 @@ downloadBtn?.addEventListener('click', async () => {
|
||||
console.error(err);
|
||||
await showAlertModal(
|
||||
'Error',
|
||||
'Error saving PDF. Check console for details.'
|
||||
trText('Error saving PDF. Check console for details.')
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -7,6 +7,8 @@ import type { PyMuPDFInstance } from '@/types';
|
||||
import JSZip from 'jszip';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const EXTENSIONS = ['.cbz', '.cbr'];
|
||||
const TOOL_NAME = 'CBZ';
|
||||
const ALL_IMAGE_EXTENSIONS = [
|
||||
@@ -99,7 +101,7 @@ async function convertImageToPng(
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
reject(new Error(trText('Failed to get canvas context')));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
@@ -108,13 +110,19 @@ async function convertImageToPng(
|
||||
if (pngBlob) {
|
||||
resolve(pngBlob);
|
||||
} else {
|
||||
reject(new Error(`Failed to convert ${filename} to PNG`));
|
||||
reject(
|
||||
new Error(
|
||||
trText('Failed to convert {{filename}} to PNG', { filename })
|
||||
)
|
||||
);
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error(`Failed to load image: ${filename}`));
|
||||
reject(
|
||||
new Error(trText('Failed to load image: {{filename}}', { filename }))
|
||||
);
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
@@ -138,7 +146,11 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
||||
const MAX_ENTRY_BYTES = 100 * 1024 * 1024;
|
||||
const MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
||||
if (imageFiles.length > MAX_CBZ_PAGES) {
|
||||
throw new Error(`Archive has too many images (max ${MAX_CBZ_PAGES}).`);
|
||||
throw new Error(
|
||||
trText('Archive has too many images (max {{MAX_CBZ_PAGES}}).', {
|
||||
MAX_CBZ_PAGES,
|
||||
})
|
||||
);
|
||||
}
|
||||
let totalBytes = 0;
|
||||
|
||||
@@ -148,12 +160,12 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
||||
zipEntry as unknown as { _data?: { uncompressedSize?: number } }
|
||||
)._data?.uncompressedSize;
|
||||
if (typeof declared === 'number' && declared > MAX_ENTRY_BYTES) {
|
||||
throw new Error('Archive contains an oversized image entry.');
|
||||
throw new Error(trText('Archive contains an oversized image entry.'));
|
||||
}
|
||||
const imageData = await zipEntry.async('arraybuffer');
|
||||
totalBytes += imageData.byteLength;
|
||||
if (totalBytes > MAX_TOTAL_BYTES) {
|
||||
throw new Error('Archive is too large when decompressed.');
|
||||
throw new Error(trText('Archive is too large when decompressed.'));
|
||||
}
|
||||
const dataArray = new Uint8Array(imageData);
|
||||
const actualFormat = detectImageFormat(dataArray);
|
||||
@@ -197,7 +209,7 @@ async function convertCbrToPdf(file: File): Promise<Blob> {
|
||||
return await pymupdf.convertToPdf(file, { filetype: 'cbz' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -272,13 +284,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{TOOL_NAME}} file.', {
|
||||
TOOL_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
showLoader(
|
||||
trText('Converting {{name}}...', { name: originalFile.name })
|
||||
);
|
||||
|
||||
let pdfBlob: Blob;
|
||||
if (isCbzFile(originalFile.name)) {
|
||||
@@ -293,7 +312,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -304,7 +325,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
let pdfBlob: Blob;
|
||||
@@ -326,7 +351,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
trText(
|
||||
'Successfully converted {{length}} {{TOOL_NAME}} file(s) to PDF.',
|
||||
{ length: state.files.length, TOOL_NAME }
|
||||
),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -336,7 +364,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { ChangePermissionsState, QpdfInstanceExtended } from '@/types';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: ChangePermissionsState = {
|
||||
file: null,
|
||||
};
|
||||
@@ -129,16 +131,17 @@ async function changePermissions() {
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
if (loaderText) loaderText.textContent = trText('Initializing...');
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
if (loaderText) loaderText.textContent = trText('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Processing PDF permissions...';
|
||||
if (loaderText)
|
||||
loaderText.textContent = trText('Processing PDF permissions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
@@ -220,11 +223,11 @@ async function changePermissions() {
|
||||
});
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
if (loaderText) loaderText.textContent = trText('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
throw new Error(trText('Processing resulted in an empty file.'));
|
||||
}
|
||||
|
||||
const blob = new Blob([new Uint8Array(outputFile)], {
|
||||
@@ -234,10 +237,11 @@ async function changePermissions() {
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
let successMessage = trText('PDF permissions changed successfully!');
|
||||
if (!shouldEncrypt) {
|
||||
successMessage =
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
successMessage = trText(
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.'
|
||||
);
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => {
|
||||
@@ -261,7 +265,11 @@ async function changePermissions() {
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${errorMessage || 'The PDF might be corrupted or password protected.'}`
|
||||
trText('An error occurred: {{error}}', {
|
||||
error:
|
||||
errorMessage ||
|
||||
trText('The PDF might be corrupted or password protected.'),
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -284,7 +292,7 @@ async function changePermissions() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -13,6 +13,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: CombineSinglePageState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -54,7 +56,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -82,7 +86,10 @@ async function updateUI() {
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
@@ -174,7 +181,12 @@ async function combineToSinglePage() {
|
||||
let currentY = finalHeight;
|
||||
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{n}} of {{length}}...', {
|
||||
n: i + 1,
|
||||
length: sourcePages.length,
|
||||
})
|
||||
);
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
|
||||
@@ -273,7 +285,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
getComparisonCacheKey,
|
||||
} from './compare-render.ts';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: CompareState = {
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
@@ -389,8 +391,8 @@ function renderChangeList() {
|
||||
if (!comparison || visibleChanges.length === 0) {
|
||||
emptyState.textContent =
|
||||
comparison?.status === 'match'
|
||||
? 'No differences detected on this page.'
|
||||
: 'No changes match the current filter.';
|
||||
? trText('No differences detected on this page.')
|
||||
: trText('No changes match the current filter.');
|
||||
emptyState.classList.remove('hidden');
|
||||
list.classList.add('hidden');
|
||||
prevChangeBtn.disabled = true;
|
||||
@@ -403,13 +405,13 @@ function renderChangeList() {
|
||||
list.classList.remove('hidden');
|
||||
|
||||
const typeLabels: Record<string, string> = {
|
||||
added: 'Added',
|
||||
removed: 'Deleted',
|
||||
modified: 'Modified',
|
||||
moved: 'Moved',
|
||||
'style-changed': 'Style Changed',
|
||||
'page-added': 'Page Added',
|
||||
'page-removed': 'Page Removed',
|
||||
added: trText('Added'),
|
||||
removed: trText('Deleted'),
|
||||
modified: trText('Modified'),
|
||||
moved: trText('Moved'),
|
||||
'style-changed': trText('Style Changed'),
|
||||
'page-added': trText('Page Added'),
|
||||
'page-removed': trText('Page Removed'),
|
||||
};
|
||||
|
||||
const grouped = new Map<
|
||||
@@ -497,7 +499,10 @@ async function buildPagePairs() {
|
||||
pageState.pdfDoc1,
|
||||
function (pageNumber, totalPages) {
|
||||
showLoader(
|
||||
`Indexing PDF 1 page ${pageNumber} of ${totalPages}...`,
|
||||
trText('Indexing PDF 1 page {{pageNumber}} of {{totalPages}}...', {
|
||||
pageNumber,
|
||||
totalPages,
|
||||
}),
|
||||
(pageNumber / Math.max(totalPages * 2, 1)) * 100
|
||||
);
|
||||
}
|
||||
@@ -506,7 +511,10 @@ async function buildPagePairs() {
|
||||
pageState.pdfDoc2,
|
||||
function (pageNumber, totalPages) {
|
||||
showLoader(
|
||||
`Indexing PDF 2 page ${pageNumber} of ${totalPages}...`,
|
||||
trText('Indexing PDF 2 page {{pageNumber}} of {{totalPages}}...', {
|
||||
pageNumber,
|
||||
totalPages,
|
||||
}),
|
||||
50 + (pageNumber / Math.max(totalPages * 2, 1)) * 100
|
||||
);
|
||||
}
|
||||
@@ -558,7 +566,10 @@ async function renderBothPages() {
|
||||
const gen = ++renderGeneration;
|
||||
|
||||
showLoader(
|
||||
`Loading comparison ${pageState.currentPage} of ${pageState.pagePairs.length}...`
|
||||
trText('Loading comparison {{currentPage}} of {{length}}...', {
|
||||
currentPage: pageState.currentPage,
|
||||
length: pageState.pagePairs.length,
|
||||
})
|
||||
);
|
||||
|
||||
const canvas1 = getElement<HTMLCanvasElement>(
|
||||
@@ -744,7 +755,7 @@ async function handleFileInput(
|
||||
hideLoader();
|
||||
const result = await loadPdfWithPasswordPrompt(file);
|
||||
if (!result) return;
|
||||
showLoader(`Loading ${result.file.name}...`);
|
||||
showLoader(trText('Loading {{name}}...', { name: result.file.name }));
|
||||
pageState[docKey] = result.pdf;
|
||||
caches.pageModelCache.clear();
|
||||
caches.comparisonCache.clear();
|
||||
@@ -792,7 +803,7 @@ async function handleFileInput(
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const backBtn = getElement<HTMLButtonElement>('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
@@ -960,7 +971,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
function updateZoomDisplay() {
|
||||
if (zoomDisplay) {
|
||||
zoomDisplay.textContent = `${Math.round(pageState.zoomLevel * 100)}%`;
|
||||
zoomDisplay.textContent = trText('{{value}}%', {
|
||||
value: Math.round(pageState.zoomLevel * 100),
|
||||
});
|
||||
}
|
||||
if (zoomOutBtn) zoomOutBtn.disabled = pageState.zoomLevel <= ZOOM_MIN;
|
||||
if (zoomInBtn) zoomInBtn.disabled = pageState.zoomLevel >= ZOOM_MAX;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { recognizePageCanvas } from '../compare/engine/ocr-page.ts';
|
||||
import { isLowQualityExtractedText } from '../compare/engine/text-normalization.ts';
|
||||
import { COMPARE_RENDER, COMPARE_GEOMETRY } from '../compare/config.ts';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
export function getElement<T extends HTMLElement>(id: string) {
|
||||
return document.getElementById(id) as T | null;
|
||||
}
|
||||
@@ -228,7 +230,7 @@ export async function renderPage(
|
||||
renderMissingPage(
|
||||
canvas,
|
||||
placeholderId,
|
||||
`Page ${pageNum} does not exist in this PDF.`
|
||||
trText('Page {{pageNum}} does not exist in this PDF.', { pageNum })
|
||||
);
|
||||
return { model: null, exists: false };
|
||||
}
|
||||
@@ -280,12 +282,12 @@ export async function renderPage(
|
||||
);
|
||||
finalModel.pageNumber = pageNum;
|
||||
} else {
|
||||
ctx.showLoader(`Running OCR on page ${pageNum}...`);
|
||||
ctx.showLoader(trText('Running OCR on page {{pageNum}}...', { pageNum }));
|
||||
const ocrModel = await recognizePageCanvas(
|
||||
canvas,
|
||||
ctx.ocrLanguage,
|
||||
function (status, progress) {
|
||||
ctx.showLoader(`OCR: ${status}`, progress * 100);
|
||||
ctx.showLoader(trText('OCR: {{status}}', { status }), progress * 100);
|
||||
}
|
||||
);
|
||||
finalModel = {
|
||||
@@ -324,7 +326,7 @@ export async function loadComparisonPage(
|
||||
renderMissingPage(
|
||||
renderTarget.canvas,
|
||||
renderTarget.placeholderId,
|
||||
'No paired page for this side.'
|
||||
trText('No paired page for this side.')
|
||||
);
|
||||
}
|
||||
return { model: null, exists: false };
|
||||
@@ -358,7 +360,7 @@ export async function loadComparisonPage(
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Could not create offscreen comparison canvas.');
|
||||
throw new Error(trText('Could not create offscreen comparison canvas.'));
|
||||
}
|
||||
|
||||
const extractedModel = await extractPageModel(page, viewport);
|
||||
|
||||
+224
-112
@@ -28,34 +28,37 @@ import {
|
||||
verifyHyperPassword,
|
||||
} from '../utils/compress.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
type OptionChangeHandler = (
|
||||
key: keyof HyperCompressOptions,
|
||||
value: number | boolean | null
|
||||
) => void;
|
||||
|
||||
const LOCK_MESSAGES = {
|
||||
locked: {
|
||||
text: 'Password protected',
|
||||
color: 'text-gray-400',
|
||||
field: 'border-gray-600 focus:ring-indigo-500',
|
||||
},
|
||||
wrong: {
|
||||
text: 'Wrong password',
|
||||
color: 'text-red-400',
|
||||
field: 'border-red-500 focus:ring-red-500',
|
||||
},
|
||||
unlocked: {
|
||||
text: 'Unlocked',
|
||||
color: 'text-green-400',
|
||||
field: 'border-green-500 focus:ring-green-500',
|
||||
},
|
||||
} as const;
|
||||
const getLockMessages = () =>
|
||||
({
|
||||
locked: {
|
||||
text: trText('Password protected'),
|
||||
color: 'text-gray-400',
|
||||
field: 'border-gray-600 focus:ring-indigo-500',
|
||||
},
|
||||
wrong: {
|
||||
text: trText('Wrong password'),
|
||||
color: 'text-red-400',
|
||||
field: 'border-red-500 focus:ring-red-500',
|
||||
},
|
||||
unlocked: {
|
||||
text: trText('Unlocked'),
|
||||
color: 'text-green-400',
|
||||
field: 'border-green-500 focus:ring-green-500',
|
||||
},
|
||||
}) as const;
|
||||
|
||||
const PASSWORD_INPUT_CLASS =
|
||||
'w-full bg-gray-800 border text-gray-200 placeholder:text-gray-500 rounded-lg pl-9 pr-10 py-2 text-sm focus:outline-none focus:ring-2 focus:border-transparent';
|
||||
|
||||
const DPI_CHOICES = [
|
||||
{ value: 0, label: 'None' },
|
||||
const getDpiChoices = () => [
|
||||
{ value: 0, label: trText('None') },
|
||||
{ value: 72, label: '72 DPI' },
|
||||
{ value: 96, label: '96 DPI' },
|
||||
{ value: 144, label: '144 DPI' },
|
||||
@@ -64,191 +67,221 @@ const DPI_CHOICES = [
|
||||
{ value: 300, label: '300 DPI' },
|
||||
];
|
||||
|
||||
const HYPER_OPTION_SECTIONS: HyperOptionSection[] = [
|
||||
const getOptionSections = (): HyperOptionSection[] => [
|
||||
{
|
||||
title: 'Images',
|
||||
title: trText('Images'),
|
||||
open: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'imageQuality',
|
||||
type: 'number',
|
||||
label: 'Image quality',
|
||||
label: trText('Image quality'),
|
||||
min: 20,
|
||||
max: 100,
|
||||
help: 'JPEG quality for re-encoded images, 20 to 100. Lower means smaller files and more visible artifacts.',
|
||||
help: trText(
|
||||
'JPEG quality for re-encoded images, 20 to 100. Lower means smaller files and more visible artifacts.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'maxDpi',
|
||||
type: 'dpi',
|
||||
label: 'Maximum resolution',
|
||||
help: 'Images above this are resampled down. None leaves resolution alone.',
|
||||
label: trText('Maximum resolution'),
|
||||
help: trText(
|
||||
'Images above this are resampled down. None leaves resolution alone.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'forceDownsample',
|
||||
type: 'bool',
|
||||
label: 'Always resample to that resolution',
|
||||
help: 'Resamples even when an image is only slightly above the limit.',
|
||||
label: trText('Always resample to that resolution'),
|
||||
help: trText(
|
||||
'Resamples even when an image is only slightly above the limit.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'grayscale',
|
||||
type: 'bool',
|
||||
label: 'Convert to grayscale',
|
||||
help: 'Reduces file size by removing color information.',
|
||||
label: trText('Convert to grayscale'),
|
||||
help: trText('Reduces file size by removing color information.'),
|
||||
},
|
||||
{
|
||||
key: 'reduceColor',
|
||||
type: 'bool',
|
||||
label: 'Reduce color complexity',
|
||||
help: 'Only where it does not change how the image looks.',
|
||||
label: trText('Reduce color complexity'),
|
||||
help: trText('Only where it does not change how the image looks.'),
|
||||
},
|
||||
{
|
||||
key: 'clipImages',
|
||||
type: 'bool',
|
||||
label: 'Clip invisible parts of images',
|
||||
label: trText('Clip invisible parts of images'),
|
||||
},
|
||||
{
|
||||
key: 'removeAlternates',
|
||||
type: 'bool',
|
||||
label: 'Remove alternate images',
|
||||
label: trText('Remove alternate images'),
|
||||
},
|
||||
{
|
||||
key: 'flattenIcc',
|
||||
type: 'bool',
|
||||
label: 'Simplify large color profiles',
|
||||
label: trText('Simplify large color profiles'),
|
||||
},
|
||||
{
|
||||
key: 'preferJpx',
|
||||
type: 'bool',
|
||||
label: 'Prefer JPEG 2000 for photos',
|
||||
help: 'Tries JPEG 2000 against JPEG for each image and keeps the smaller one.',
|
||||
label: trText('Prefer JPEG 2000 for photos'),
|
||||
help: trText(
|
||||
'Tries JPEG 2000 against JPEG for each image and keeps the smaller one.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'lossless',
|
||||
type: 'bool',
|
||||
label: 'Lossless (no image re-encoding)',
|
||||
help: 'Turns off quality, resolution and every other lossy image step.',
|
||||
label: trText('Lossless (no image re-encoding)'),
|
||||
help: trText(
|
||||
'Turns off quality, resolution and every other lossy image step.'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Fonts',
|
||||
title: trText('Fonts'),
|
||||
fields: [
|
||||
{
|
||||
key: 'subsetFonts',
|
||||
type: 'bool',
|
||||
label: 'Subset embedded fonts',
|
||||
help: 'Keeps only the characters the document uses.',
|
||||
label: trText('Subset embedded fonts'),
|
||||
help: trText('Keeps only the characters the document uses.'),
|
||||
},
|
||||
{
|
||||
key: 'mergeFonts',
|
||||
type: 'bool',
|
||||
label: trText('Merge duplicate fonts'),
|
||||
},
|
||||
{ key: 'mergeFonts', type: 'bool', label: 'Merge duplicate fonts' },
|
||||
{
|
||||
key: 'removeStandardFonts',
|
||||
type: 'bool',
|
||||
label: 'Unembed standard fonts',
|
||||
help: 'PDF readers supply the 14 standard PDF fonts themselves.',
|
||||
label: trText('Unembed standard fonts'),
|
||||
help: trText(
|
||||
'PDF readers supply the 14 standard PDF fonts themselves.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'unembedAliasedFonts',
|
||||
type: 'bool',
|
||||
label: 'Unembed standard-font look-alikes',
|
||||
help: 'Arial, Times New Roman and Courier New map to the built-in equivalents. Only applied when safe.',
|
||||
label: trText('Unembed standard-font look-alikes'),
|
||||
help: trText(
|
||||
'Arial, Times New Roman and Courier New map to the built-in equivalents. Only applied when safe.'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Content',
|
||||
title: trText('Content'),
|
||||
fields: [
|
||||
{
|
||||
key: 'removeAnnots',
|
||||
type: 'bool',
|
||||
label: 'Flatten annotations',
|
||||
help: 'Comments and markup become part of the page. Form fields and links keep working.',
|
||||
label: trText('Flatten annotations'),
|
||||
help: trText(
|
||||
'Comments and markup become part of the page. Form fields and links keep working.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'flattenForms',
|
||||
type: 'bool',
|
||||
label: 'Flatten form fields',
|
||||
help: 'Form fields stop being fillable.',
|
||||
label: trText('Flatten form fields'),
|
||||
help: trText('Form fields stop being fillable.'),
|
||||
},
|
||||
{
|
||||
key: 'flattenLinks',
|
||||
type: 'bool',
|
||||
label: 'Flatten links',
|
||||
help: 'Links stop being clickable.',
|
||||
label: trText('Flatten links'),
|
||||
help: trText('Links stop being clickable.'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Cleanup',
|
||||
title: trText('Cleanup'),
|
||||
fields: [
|
||||
{
|
||||
key: 'removeThumbnails',
|
||||
type: 'bool',
|
||||
label: 'Remove page thumbnails',
|
||||
label: trText('Remove page thumbnails'),
|
||||
},
|
||||
{
|
||||
key: 'removeAppData',
|
||||
type: 'bool',
|
||||
label: 'Remove application data',
|
||||
help: 'Private data that editing apps store inside the PDF.',
|
||||
label: trText('Remove application data'),
|
||||
help: trText('Private data that editing apps store inside the PDF.'),
|
||||
},
|
||||
{
|
||||
key: 'removeThreads',
|
||||
type: 'bool',
|
||||
label: trText('Remove article threads'),
|
||||
},
|
||||
{ key: 'removeThreads', type: 'bool', label: 'Remove article threads' },
|
||||
{
|
||||
key: 'removeSpiderInfo',
|
||||
type: 'bool',
|
||||
label: 'Remove web capture information',
|
||||
label: trText('Remove web capture information'),
|
||||
},
|
||||
{
|
||||
key: 'removeStructTree',
|
||||
type: 'bool',
|
||||
label: 'Remove document structure tree',
|
||||
help: 'Drops tagging, which screen readers rely on.',
|
||||
label: trText('Remove document structure tree'),
|
||||
help: trText('Drops tagging, which screen readers rely on.'),
|
||||
},
|
||||
{
|
||||
key: 'removeOutputIntents',
|
||||
type: 'bool',
|
||||
label: 'Remove output intents',
|
||||
label: trText('Remove output intents'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Rasterize',
|
||||
title: trText('Rasterize'),
|
||||
fields: [
|
||||
{
|
||||
key: 'rasterizePages',
|
||||
type: 'bool',
|
||||
label: 'Rasterize pages to images',
|
||||
help: 'For image-only PDFs such as scans. Skipped when the document has selectable text, unless Override content protection is on.',
|
||||
label: trText('Rasterize pages to images'),
|
||||
help: trText(
|
||||
'For image-only PDFs such as scans. Skipped when the document has selectable text, unless Override content protection is on.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'rasterizeDpi',
|
||||
type: 'number',
|
||||
label: 'Rasterize resolution (DPI)',
|
||||
label: trText('Rasterize resolution (DPI)'),
|
||||
min: 36,
|
||||
max: 600,
|
||||
},
|
||||
{
|
||||
key: 'rasterizeQuality',
|
||||
type: 'number',
|
||||
label: 'Rasterize quality',
|
||||
label: trText('Rasterize quality'),
|
||||
min: 1,
|
||||
max: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Output',
|
||||
title: trText('Output'),
|
||||
fields: [
|
||||
{
|
||||
key: 'preserveConformance',
|
||||
type: 'bool',
|
||||
label: 'Preserve PDF/A',
|
||||
help: 'Keeps a PDF/A file conformant. Brotli and rasterizing are skipped while this is on.',
|
||||
label: trText('Preserve PDF/A'),
|
||||
help: trText(
|
||||
'Keeps a PDF/A file conformant. Brotli and rasterizing are skipped while this is on.'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'brotli',
|
||||
type: 'bool',
|
||||
label: 'Brotli compression (PDF 2.0)',
|
||||
help: 'Smaller files, but only readers with Brotli support can open them, such as Firefox, MuPDF 1.26+ and Ghostscript 10.06+.',
|
||||
label: trText('Brotli compression (PDF 2.0)'),
|
||||
help: trText(
|
||||
'Smaller files, but only readers with Brotli support can open them, such as Firefox, MuPDF 1.26+ and Ghostscript 10.06+.'
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -303,7 +336,7 @@ function buildOptionField(
|
||||
const select = document.createElement('select');
|
||||
select.id = id;
|
||||
select.className = controlClass;
|
||||
for (const choice of DPI_CHOICES) {
|
||||
for (const choice of getDpiChoices()) {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(choice.value);
|
||||
option.textContent = choice.label;
|
||||
@@ -355,7 +388,7 @@ function renderHyperOptions(
|
||||
onChange: OptionChangeHandler
|
||||
): (options: HyperCompressOptions) => void {
|
||||
const syncs: ((options: HyperCompressOptions) => void)[] = [];
|
||||
for (const section of HYPER_OPTION_SECTIONS) {
|
||||
for (const section of getOptionSections()) {
|
||||
const details = document.createElement('details');
|
||||
details.className = 'group rounded-lg border border-gray-700';
|
||||
details.open = Boolean(section.open);
|
||||
@@ -393,21 +426,41 @@ function describeCompressResult(
|
||||
if (saved > 0) {
|
||||
const percent = ((saved / result.originalSize) * 100).toFixed(1);
|
||||
parts.push(
|
||||
`File size reduced from ${formatBytes(result.originalSize)} to ${formatBytes(result.compressedSize)} (Saved ${percent}%).`
|
||||
trText(
|
||||
'File size reduced from {{originalSize}} to {{compressedSize}} (Saved {{percent}}%).',
|
||||
{
|
||||
originalSize: formatBytes(result.originalSize),
|
||||
compressedSize: formatBytes(result.compressedSize),
|
||||
percent,
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
`Could not reduce file size further. Original: ${formatBytes(result.originalSize)}, New: ${formatBytes(result.compressedSize)}.`
|
||||
trText(
|
||||
'Could not reduce file size further. Original: {{originalSize}}, New: {{compressedSize}}.',
|
||||
{
|
||||
originalSize: formatBytes(result.originalSize),
|
||||
compressedSize: formatBytes(result.compressedSize),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const preservingPdfa = options.preserveConformance && result.pdfa !== null;
|
||||
if (targetBytes !== undefined) {
|
||||
if (result.metTarget) {
|
||||
parts.push(`It fits the ${formatBytes(targetBytes)} target.`);
|
||||
parts.push(
|
||||
trText('It fits the {{targetBytes}} target.', {
|
||||
targetBytes: formatBytes(targetBytes),
|
||||
})
|
||||
);
|
||||
} else if (preservingPdfa) {
|
||||
parts.push(
|
||||
`It is still above the ${formatBytes(targetBytes)} target because Preserve PDF/A limits how far it can be compressed.`
|
||||
trText(
|
||||
'It is still above the {{targetBytes}} target because Preserve PDF/A limits how far it can be compressed.',
|
||||
{ targetBytes: formatBytes(targetBytes) }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
parts.push(
|
||||
@@ -418,34 +471,38 @@ function describeCompressResult(
|
||||
|
||||
if (result.signaturesRemoved) {
|
||||
parts.push(
|
||||
'Its digital signatures were removed so it could be compressed. Sign it again if needed.'
|
||||
trText(
|
||||
'Its digital signatures were removed so it could be compressed. Sign it again if needed.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (result.pdfaOutcome === 'claim-withdrawn') {
|
||||
parts.push(
|
||||
'It no longer claims PDF/A conformance. Turn on Preserve PDF/A under Custom Settings to keep it.'
|
||||
trText(
|
||||
'It no longer claims PDF/A conformance. Turn on Preserve PDF/A under Custom Settings to keep it.'
|
||||
)
|
||||
);
|
||||
} else if (preservingPdfa && (options.brotli || options.rasterizePages)) {
|
||||
parts.push('Brotli and rasterizing were skipped to keep it PDF/A.');
|
||||
parts.push(trText('Brotli and rasterizing were skipped to keep it PDF/A.'));
|
||||
}
|
||||
|
||||
if (saved <= 0) {
|
||||
return {
|
||||
title: 'Compression Finished',
|
||||
title: trText('Compression Finished'),
|
||||
message: parts.join(' '),
|
||||
type: 'warning',
|
||||
};
|
||||
}
|
||||
if (result.metTarget === false) {
|
||||
return {
|
||||
title: 'Target Not Reached',
|
||||
title: trText('Target Not Reached'),
|
||||
message: parts.join(' '),
|
||||
type: 'warning',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Compression Complete',
|
||||
title: trText('Compression Complete'),
|
||||
message: parts.join(' '),
|
||||
type: 'success',
|
||||
};
|
||||
@@ -477,11 +534,11 @@ function createPasswordField(
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'password';
|
||||
input.placeholder = 'Enter password';
|
||||
input.placeholder = trText('Enter password');
|
||||
input.autocomplete = 'off';
|
||||
input.value = value;
|
||||
input.setAttribute('aria-label', label);
|
||||
input.className = `${PASSWORD_INPUT_CLASS} ${LOCK_MESSAGES.locked.field}`;
|
||||
input.className = `${PASSWORD_INPUT_CLASS} ${getLockMessages().locked.field}`;
|
||||
input.addEventListener('input', () => onInput(input.value));
|
||||
|
||||
const showIcon = createIcon('eye', '');
|
||||
@@ -490,14 +547,14 @@ function createPasswordField(
|
||||
toggle.type = 'button';
|
||||
toggle.className =
|
||||
'absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 hover:text-gray-200';
|
||||
toggle.setAttribute('aria-label', 'Show password');
|
||||
toggle.setAttribute('aria-label', trText('Show password'));
|
||||
toggle.append(showIcon, hideIcon);
|
||||
toggle.addEventListener('click', () => {
|
||||
const reveal = input.type === 'password';
|
||||
input.type = reveal ? 'text' : 'password';
|
||||
toggle.setAttribute(
|
||||
'aria-label',
|
||||
reveal ? 'Hide password' : 'Show password'
|
||||
reveal ? trText('Hide password') : trText('Show password')
|
||||
);
|
||||
showIcon.classList.toggle('hidden', reveal);
|
||||
hideIcon.classList.toggle('hidden', !reveal);
|
||||
@@ -517,7 +574,7 @@ function createIcon(name: string, className: string): HTMLElement {
|
||||
return icon;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const compressOptions = document.getElementById('compress-options');
|
||||
@@ -652,12 +709,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
(file) => fileLocks.get(file)?.status === 'unlocked'
|
||||
).length;
|
||||
if (sharedMissed && unlocked < files.length) {
|
||||
sharedView.status.textContent =
|
||||
'That password did not open any of the remaining files.';
|
||||
sharedView.status.textContent = trText(
|
||||
'That password did not open any of the remaining files.'
|
||||
);
|
||||
sharedView.status.className = 'text-xs mt-1 text-red-400';
|
||||
return;
|
||||
}
|
||||
sharedView.status.textContent = `Unlocked ${unlocked} of ${files.length} files.`;
|
||||
sharedView.status.textContent = trText(
|
||||
'Unlocked {{unlocked}} of {{length}} files.',
|
||||
{ unlocked, length: files.length }
|
||||
);
|
||||
sharedView.status.className = `text-xs mt-1 ${unlocked === files.length ? 'text-green-400' : 'text-gray-400'}`;
|
||||
};
|
||||
|
||||
@@ -665,7 +726,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const lock = fileLocks.get(file);
|
||||
const view = lockViews.get(file);
|
||||
if (lock && view && lock.status !== 'checking' && lock.status !== 'open') {
|
||||
const { text, color, field } = LOCK_MESSAGES[lock.status];
|
||||
const { text, color, field } = getLockMessages()[lock.status];
|
||||
if (view.input.value !== lock.password) view.input.value = lock.password;
|
||||
view.status.textContent = text;
|
||||
view.status.className = `truncate ${color}`;
|
||||
@@ -736,9 +797,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
sharedDiv.className = 'bg-gray-700 p-3 rounded-lg text-sm';
|
||||
const sharedLabel = document.createElement('div');
|
||||
sharedLabel.className = 'font-medium text-gray-200 text-sm';
|
||||
sharedLabel.textContent = 'Password for all locked files';
|
||||
sharedLabel.textContent = trText('Password for all locked files');
|
||||
sharedView = createPasswordField(
|
||||
'Password for all locked files',
|
||||
trText('Password for all locked files'),
|
||||
sharedPassword,
|
||||
applySharedPassword
|
||||
);
|
||||
@@ -774,7 +835,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (locked) {
|
||||
const view = createPasswordField(
|
||||
`Password for ${file.name}`,
|
||||
trText('Password for {{name}}', { name: file.name }),
|
||||
lock.password,
|
||||
(value) => {
|
||||
lock.password = value;
|
||||
@@ -785,7 +846,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
view.root.classList.add('mt-3');
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 whitespace-nowrap';
|
||||
sizeSpan.textContent = `${formatBytes(file.size)} ·`;
|
||||
sizeSpan.textContent = trText('{{size}} ·', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
metaSpan.className = 'flex gap-1 min-w-0 text-xs text-gray-400';
|
||||
metaSpan.replaceChildren(sizeSpan, view.status);
|
||||
fileDiv.append(view.root);
|
||||
@@ -795,7 +858,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.setAttribute('aria-label', `Remove ${file.name}`);
|
||||
removeBtn.setAttribute(
|
||||
'aria-label',
|
||||
trText('Remove {{name}}', { name: file.name })
|
||||
);
|
||||
removeBtn.append(createIcon('trash', ''));
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((f) => f !== file);
|
||||
@@ -842,7 +908,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader(
|
||||
targetBytes === undefined
|
||||
? 'Compressing...'
|
||||
: `Compressing to ${formatBytes(targetBytes)}...`
|
||||
: trText('Compressing to {{size}}...', {
|
||||
size: formatBytes(targetBytes),
|
||||
})
|
||||
);
|
||||
const result = await compressWithHyper(await file.arrayBuffer(), {
|
||||
options: runOptions,
|
||||
@@ -854,7 +922,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (result.signed) {
|
||||
showAlert(
|
||||
'Signed PDF Not Changed',
|
||||
`${file.name} is digitally signed, so it was left as it is. Compressing it would break the signature. To compress it anyway, turn on Override content protection under Custom Settings; its signatures will be removed.`,
|
||||
trText(
|
||||
'{{name}} is digitally signed, so it was left as it is. Compressing it would break the signature. To compress it anyway, turn on Override content protection under Custom Settings; its signatures will be removed.',
|
||||
{ name: file.name }
|
||||
),
|
||||
'warning',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -873,7 +944,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showAlert(
|
||||
title,
|
||||
password
|
||||
? `${message} It was password protected, so the compressed copy has no password. Use Protect PDF to add one again.`
|
||||
? trText(
|
||||
'{{message}} It was password protected, so the compressed copy has no password. Use Protect PDF to add one again.',
|
||||
{ message }
|
||||
)
|
||||
: message,
|
||||
type,
|
||||
() => resetState()
|
||||
@@ -901,7 +975,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
showLoader(`Compressing ${i + 1}/${files.length}: ${file.name}...`);
|
||||
showLoader(
|
||||
trText('Compressing {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
const lock = fileLocks.get(file);
|
||||
const password = isProtected(lock) ? lock.password : '';
|
||||
try {
|
||||
@@ -931,7 +1011,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`None of the files could be compressed: ${failures.join(', ')}.`
|
||||
trText('None of the files could be compressed: {{join}}.', {
|
||||
join: failures.join(', '),
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -944,8 +1026,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const totalSavings = totalOriginalSize - totalCompressedSize;
|
||||
const parts = [
|
||||
totalSavings > 0
|
||||
? `Compressed ${compressedCount} PDF(s). Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${((totalSavings / totalOriginalSize) * 100).toFixed(1)}%).`
|
||||
: `Compressed ${compressedCount} PDF(s). Total size: ${formatBytes(totalCompressedSize)}.`,
|
||||
? trText(
|
||||
'Compressed {{compressedCount}} PDF(s). Total size reduced from {{totalOriginalSize}} to {{totalCompressedSize}} (Saved {{value}}%).',
|
||||
{
|
||||
compressedCount,
|
||||
totalOriginalSize: formatBytes(totalOriginalSize),
|
||||
totalCompressedSize: formatBytes(totalCompressedSize),
|
||||
value: ((totalSavings / totalOriginalSize) * 100).toFixed(1),
|
||||
}
|
||||
)
|
||||
: trText(
|
||||
'Compressed {{compressedCount}} PDF(s). Total size: {{totalCompressedSize}}.',
|
||||
{
|
||||
compressedCount,
|
||||
totalCompressedSize: formatBytes(totalCompressedSize),
|
||||
}
|
||||
),
|
||||
];
|
||||
if (signedCount > 0) {
|
||||
parts.push(
|
||||
@@ -973,7 +1069,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
parts.push(`Skipped: ${failures.join(', ')}.`);
|
||||
parts.push(trText('Skipped: {{join}}.', { join: failures.join(', ') }));
|
||||
}
|
||||
|
||||
const hasProblems = failures.length > 0 || missedTargetCount > 0;
|
||||
@@ -999,8 +1095,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showAlert(
|
||||
'Password Needed',
|
||||
needPassword.length === 1
|
||||
? `${needPassword[0].name} is password protected. Enter its password on the file card, then compress again.`
|
||||
: `These files are password protected: ${needPassword.map((file) => file.name).join(', ')}. Enter their passwords on the file cards, or remove them, then compress again.`,
|
||||
? trText(
|
||||
'{{name}} is password protected. Enter its password on the file card, then compress again.',
|
||||
{ name: needPassword[0].name }
|
||||
)
|
||||
: trText(
|
||||
'These files are password protected: {{names}}. Enter their passwords on the file cards, or remove them, then compress again.',
|
||||
{ names: needPassword.map((file) => file.name).join(', ') }
|
||||
),
|
||||
'warning',
|
||||
() => lockViews.get(needPassword[0])?.input.focus()
|
||||
);
|
||||
@@ -1032,7 +1134,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateUI();
|
||||
showAlert(
|
||||
'Password Needed',
|
||||
`${file.name} is password protected. Enter its password on the file card, then compress again.`,
|
||||
trText(
|
||||
'{{name}} is password protected. Enter its password on the file card, then compress again.',
|
||||
{ name: file.name }
|
||||
),
|
||||
'warning',
|
||||
() => lockViews.get(file)?.input.focus()
|
||||
);
|
||||
@@ -1041,7 +1146,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
console.error('[CompressPDF] Error:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during compression. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during compression. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1061,8 +1168,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showAlert(
|
||||
'Not a PDF',
|
||||
rejected.length === 1
|
||||
? `${rejected[0]} is not a valid PDF, so it was not added.`
|
||||
: `These files are not valid PDFs, so they were not added: ${rejected.join(', ')}.`,
|
||||
? trText('{{name}} is not a valid PDF, so it was not added.', {
|
||||
name: rejected[0],
|
||||
})
|
||||
: trText(
|
||||
'These files are not valid PDFs, so they were not added: {{names}}.',
|
||||
{ names: rejected.join(', ') }
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { CropperState, CropPercentages } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const cropperState: CropperState = {
|
||||
pdfDoc: null,
|
||||
currentPageNum: 1,
|
||||
@@ -18,11 +20,7 @@ const cropperState: CropperState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -124,7 +122,10 @@ function updateFileDisplay() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(cropperState.file.size)} • ${cropperState.pdfDoc?.numPages || 0} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{value}} pages', {
|
||||
size: formatBytes(cropperState.file.size),
|
||||
value: cropperState.pdfDoc?.numPages || 0,
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -153,7 +154,7 @@ function saveCurrentCrop() {
|
||||
}
|
||||
|
||||
async function displayPageAsImage(num: number) {
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
showLoader(trText('Rendering Page {{num}}...', { num }));
|
||||
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
@@ -226,7 +227,10 @@ async function changePage(offset: number) {
|
||||
function updatePageInfo() {
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
if (pageInfo)
|
||||
pageInfo.textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
pageInfo.textContent = trText('Page {{currentPageNum}} of {{numPages}}', {
|
||||
currentPageNum: cropperState.currentPageNum,
|
||||
numPages: cropperState.pdfDoc.numPages,
|
||||
});
|
||||
}
|
||||
|
||||
function enableControls() {
|
||||
@@ -353,7 +357,12 @@ async function performFlatteningCrop(
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{pageNum}} of {{totalPages}}...', {
|
||||
pageNum,
|
||||
totalPages,
|
||||
})
|
||||
);
|
||||
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
|
||||
+16
-4
@@ -15,6 +15,8 @@ import type { CropPercentages } from '@/types';
|
||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
const cropperState: {
|
||||
pdfDoc: PDFDocumentProxy | null;
|
||||
currentPageNum: number;
|
||||
@@ -54,7 +56,7 @@ function saveCurrentCrop() {
|
||||
*/
|
||||
|
||||
async function displayPageAsImage(num: number) {
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
showLoader(trText('Rendering Page {{num}}...', { num }));
|
||||
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
@@ -130,8 +132,13 @@ async function changePage(offset: number) {
|
||||
}
|
||||
|
||||
function updatePageInfo() {
|
||||
document.getElementById('page-info').textContent =
|
||||
`Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
document.getElementById('page-info').textContent = trText(
|
||||
'Page {{currentPageNum}} of {{numPages}}',
|
||||
{
|
||||
currentPageNum: cropperState.currentPageNum,
|
||||
numPages: cropperState.pdfDoc.numPages,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function enableControls() {
|
||||
@@ -216,7 +223,12 @@ async function performFlatteningCrop(
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{pageNum}} of {{totalPages}}...', {
|
||||
pageNum,
|
||||
totalPages,
|
||||
})
|
||||
);
|
||||
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
|
||||
@@ -3,7 +3,9 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -123,7 +125,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -145,7 +149,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const overallPercent =
|
||||
(i / state.files.length) * 100 + percent / state.files.length;
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`,
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
}),
|
||||
overallPercent
|
||||
);
|
||||
},
|
||||
@@ -172,7 +180,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} CSV file(s) to PDF.`,
|
||||
trText('Successfully converted {{length}} CSV file(s) to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -186,7 +196,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ import { DeletePagesState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const deleteState: DeletePagesState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -20,11 +22,7 @@ const deleteState: DeletePagesState = {
|
||||
pagesToDelete: new Set(),
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -124,7 +122,10 @@ function updateFileDisplay() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(deleteState.file.size)} • ${deleteState.totalPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{totalPages}} pages', {
|
||||
size: formatBytes(deleteState.file.size),
|
||||
totalPages: deleteState.totalPages,
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -177,7 +178,7 @@ async function renderThumbnails() {
|
||||
const pageLabel = document.createElement('span');
|
||||
pageLabel.className =
|
||||
'absolute top-1 left-1 bg-gray-800 text-white text-xs px-1.5 py-0.5 rounded';
|
||||
pageLabel.textContent = `${i}`;
|
||||
pageLabel.textContent = trText('{{i}}', { i });
|
||||
|
||||
const deleteOverlay = document.createElement('div');
|
||||
deleteOverlay.className =
|
||||
@@ -273,7 +274,9 @@ async function deletePages() {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
`Deleted ${deleteState.pagesToDelete.size} page(s) successfully!`,
|
||||
trText('Deleted {{size}} page(s) successfully!', {
|
||||
size: deleteState.pagesToDelete.size,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import { downloadFile } from '../utils/helpers';
|
||||
import { isWasmAvailable } from '../config/wasm-cdn-config.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
|
||||
import { trText, trHtml, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface DeskewResult {
|
||||
totalPages: number;
|
||||
correctedPages: number;
|
||||
@@ -27,7 +29,7 @@ function showLoader(message: string): void {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
const text = document.getElementById('loader-text');
|
||||
if (loader && text) {
|
||||
text.textContent = message;
|
||||
text.textContent = trText(message);
|
||||
loader.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
@@ -44,8 +46,8 @@ function showAlert(title: string, message: string): void {
|
||||
const titleEl = document.getElementById('alert-title');
|
||||
const msgEl = document.getElementById('alert-message');
|
||||
if (modal && titleEl && msgEl) {
|
||||
titleEl.textContent = title;
|
||||
msgEl.textContent = message;
|
||||
titleEl.textContent = trText(title);
|
||||
msgEl.textContent = trText(message);
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
@@ -90,7 +92,9 @@ function updateFileDisplay(): void {
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-gray-500 text-sm';
|
||||
sizeSpan.textContent = `(${(file.size / 1024).toFixed(1)} KB)`;
|
||||
sizeSpan.textContent = trText('({{value}} KB)', {
|
||||
value: (file.size / 1024).toFixed(1),
|
||||
});
|
||||
|
||||
info.append(fileIcon, nameSpan, sizeSpan);
|
||||
|
||||
@@ -141,9 +145,9 @@ function displayResults(result: DeskewResult): void {
|
||||
return `
|
||||
<div class="flex items-center gap-2 text-sm py-1">
|
||||
<i data-lucide="${icon}" class="w-4 h-4 ${color}"></i>
|
||||
<span class="text-gray-300">Page ${idx + 1}:</span>
|
||||
<span class="text-gray-300">${trHtml('Page {{n}}:', { n: idx + 1 })}</span>
|
||||
<span class="${color}">${angle.toFixed(2)}°</span>
|
||||
${wasCorrected ? '<span class="text-green-400 text-xs">(corrected)</span>' : ''}
|
||||
${wasCorrected ? `<span class="text-green-400 text-xs">${trHtml('(corrected)')}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
@@ -181,7 +185,7 @@ async function processDeskew(): Promise<void> {
|
||||
await pdf.load();
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
showLoader(`Deskewing ${file.name}...`);
|
||||
showLoader(trText('Deskewing {{name}}...', { name: file.name }));
|
||||
|
||||
const { pdf: resultPdf, result } = await pdf.deskewPdf(file, {
|
||||
threshold,
|
||||
@@ -196,14 +200,22 @@ async function processDeskew(): Promise<void> {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
`Deskewed ${selectedFiles.length} file(s). ${selectedFiles.length > 1 ? 'Downloads started for all files.' : ''}`
|
||||
trText('Deskewed {{length}} file(s). {{value}}', {
|
||||
length: selectedFiles.length,
|
||||
value:
|
||||
selectedFiles.length > 1
|
||||
? trText('Downloads started for all files.')
|
||||
: '',
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
hideLoader();
|
||||
console.error('Deskew error:', error);
|
||||
showAlert(
|
||||
'Error',
|
||||
`Failed to deskew PDF: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
trText('Failed to deskew PDF: {{error}}', {
|
||||
error: error instanceof Error ? error.message : trText('Unknown error'),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -280,4 +292,4 @@ function initPage(): void {
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
onPageReady(initPage);
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { sanitizeTranslationHtml } from '../utils/translation-html.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
signPdf,
|
||||
parsePfxFile,
|
||||
@@ -286,7 +286,10 @@ async function updatePdfDisplay(): Promise<void> {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(state.pdfFile.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -318,7 +321,12 @@ async function updatePdfDisplay(): Promise<void> {
|
||||
state.pdfFile = result.file;
|
||||
state.pdfBytes = new Uint8Array(result.bytes);
|
||||
nameSpan.textContent = result.file.name;
|
||||
metaSpan.textContent = `${formatBytes(result.file.size)} • ${t('tools:digitalSignPdf.pageCount', { count: result.pdf.numPages })}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}}', {
|
||||
size: formatBytes(result.file.size),
|
||||
numPages: t('tools:digitalSignPdf.pageCount', {
|
||||
count: result.pdf.numPages,
|
||||
}),
|
||||
});
|
||||
result.pdf.destroy();
|
||||
}
|
||||
}
|
||||
@@ -575,7 +583,10 @@ async function handlePasswordInput(): Promise<void> {
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t('tools:digitalSignPdf.invalidPasswordOrCertificate');
|
||||
certStatus.textContent = errorMessage.includes('password')
|
||||
const passwordProblem =
|
||||
error instanceof Error &&
|
||||
(error.name === 'PasswordError' || /password/i.test(errorMessage));
|
||||
certStatus.textContent = passwordProblem
|
||||
? t('tools:digitalSignPdf.incorrectPassword')
|
||||
: t('tools:digitalSignPdf.failedParseCertificate');
|
||||
certStatus.className = 'text-xs text-red-400';
|
||||
@@ -603,7 +614,10 @@ function updateCertInfo(): void {
|
||||
}
|
||||
if (certValidity) {
|
||||
const formatDate = (date: Date) => date.toLocaleDateString();
|
||||
certValidity.textContent = `${formatDate(info.validFrom)} - ${formatDate(info.validTo)}`;
|
||||
certValidity.textContent = trText('{{validFrom}} - {{validTo}}', {
|
||||
validFrom: formatDate(info.validFrom),
|
||||
validTo: formatDate(info.validTo),
|
||||
});
|
||||
}
|
||||
|
||||
certInfo.classList.remove('hidden');
|
||||
@@ -787,8 +801,4 @@ async function processSignature(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
@@ -3,6 +3,8 @@ import forge from 'node-forge';
|
||||
import { CertificateData, SignPdfOptions } from '@/types';
|
||||
import { isValidTsaRequestUrl } from '../config/timestamp-tsa.js';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
export function parsePfxFile(
|
||||
pfxBytes: ArrayBuffer,
|
||||
password: string
|
||||
@@ -19,17 +21,17 @@ export function parsePfxFile(
|
||||
const keyBagArray = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
|
||||
|
||||
if (!certBagArray || certBagArray.length === 0) {
|
||||
throw new Error('No certificate found in PFX file');
|
||||
throw new Error(trText('No certificate found in PFX file'));
|
||||
}
|
||||
|
||||
if (!keyBagArray || keyBagArray.length === 0) {
|
||||
throw new Error('No private key found in PFX file');
|
||||
throw new Error(trText('No private key found in PFX file'));
|
||||
}
|
||||
|
||||
const certificate = certBagArray[0].cert;
|
||||
|
||||
if (!certificate) {
|
||||
throw new Error('Failed to extract certificate from PFX file');
|
||||
throw new Error(trText('Failed to extract certificate from PFX file'));
|
||||
}
|
||||
|
||||
return { p12Buffer: pfxBytes, password, certificate };
|
||||
@@ -45,11 +47,17 @@ export function parsePemFiles(
|
||||
let privateKey: forge.pki.PrivateKey;
|
||||
if (keyPem.includes('ENCRYPTED')) {
|
||||
if (!keyPassword) {
|
||||
throw new Error('Password required for encrypted private key');
|
||||
const error = new Error(
|
||||
trText('Password required for encrypted private key')
|
||||
);
|
||||
error.name = 'PasswordError';
|
||||
throw error;
|
||||
}
|
||||
privateKey = forge.pki.decryptRsaPrivateKey(keyPem, keyPassword);
|
||||
if (!privateKey) {
|
||||
throw new Error('Failed to decrypt private key');
|
||||
const error = new Error(trText('Failed to decrypt private key'));
|
||||
error.name = 'PasswordError';
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
privateKey = forge.pki.privateKeyFromPem(keyPem);
|
||||
@@ -83,11 +91,11 @@ export function parseCombinedPem(
|
||||
);
|
||||
|
||||
if (!certMatch) {
|
||||
throw new Error('No certificate found in PEM file');
|
||||
throw new Error(trText('No certificate found in PEM file'));
|
||||
}
|
||||
|
||||
if (!keyMatch) {
|
||||
throw new Error('No private key found in PEM file');
|
||||
throw new Error(trText('No private key found in PEM file'));
|
||||
}
|
||||
|
||||
return parsePemFiles(certMatch[0], keyMatch[0], password);
|
||||
@@ -378,7 +386,9 @@ export async function timestampPdf(
|
||||
): Promise<Uint8Array> {
|
||||
if (!isValidTsaRequestUrl(tsaUrl)) {
|
||||
throw new Error(
|
||||
`Invalid TSA URL. The timestamp authority must be a valid http:// or https:// URL.`
|
||||
trText(
|
||||
'Invalid TSA URL. The timestamp authority must be a valid http:// or https:// URL.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -436,8 +446,8 @@ export function getCertificateInfo(certificate: forge.pki.Certificate): {
|
||||
const issuerCN = certificate.issuer.getField('CN');
|
||||
|
||||
return {
|
||||
subject: (subjectCN?.value as string) ?? 'Unknown',
|
||||
issuer: (issuerCN?.value as string) ?? 'Unknown',
|
||||
subject: (subjectCN?.value as string) ?? trText('Unknown'),
|
||||
issuer: (issuerCN?.value as string) ?? trText('Unknown'),
|
||||
validFrom: certificate.validity.notBefore,
|
||||
validTo: certificate.validity.notAfter,
|
||||
serialNumber: certificate.serialNumber,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: DividePagesState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -63,7 +65,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -92,7 +96,10 @@ async function updateUI() {
|
||||
pageState.totalPages = pageState.pdfDoc.getPageCount();
|
||||
hideLoader();
|
||||
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.totalPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{totalPages}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
totalPages: pageState.totalPages,
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
@@ -151,7 +158,12 @@ async function dividePages() {
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{pageNum}} of {{length}}...', {
|
||||
pageNum,
|
||||
length: pages.length,
|
||||
})
|
||||
);
|
||||
|
||||
if (pagesToDivide.has(pageNum)) {
|
||||
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
@@ -211,7 +223,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -3,7 +3,7 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { formatBytes, downloadFile } from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, onPageReady } from '../i18n/index.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
@@ -28,11 +28,7 @@ const translate = (
|
||||
return translation && translation !== key ? translation : fallback;
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
/**
|
||||
* Wires duplex-collate page event handlers and initial UI behavior.
|
||||
|
||||
@@ -12,6 +12,8 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
const duplicateOrganizeState: {
|
||||
sortableInstances: { pageGrid?: Sortable };
|
||||
} = {
|
||||
@@ -133,7 +135,7 @@ export async function renderDuplicateOrganizeThumbnails() {
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className =
|
||||
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
duplicateBtn.title = 'Duplicate Page';
|
||||
duplicateBtn.title = trText('Duplicate Page');
|
||||
const duplicateIcon = document.createElement('i');
|
||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||
duplicateIcon.className = 'w-5 h-5';
|
||||
@@ -142,7 +144,7 @@ export async function renderDuplicateOrganizeThumbnails() {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className =
|
||||
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
deleteBtn.title = 'Delete Page';
|
||||
deleteBtn.title = trText('Delete Page');
|
||||
const deleteIcon = document.createElement('i');
|
||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||
deleteIcon.className = 'w-5 h-5';
|
||||
@@ -163,7 +165,12 @@ export async function renderDuplicateOrganizeThumbnails() {
|
||||
useLazyLoading: true,
|
||||
lazyLoadMargin: '400px',
|
||||
onProgress: (current, total) => {
|
||||
showLoader(`Rendering page previews: ${current}/${total}`);
|
||||
showLoader(
|
||||
trText('Rendering page previews: {{current}}/{{total}}', {
|
||||
current,
|
||||
total,
|
||||
})
|
||||
);
|
||||
},
|
||||
onBatchComplete: () => {
|
||||
createIcons({ icons });
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '../utils/wasm-provider.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const worker = new Worker(
|
||||
import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'
|
||||
);
|
||||
@@ -96,7 +98,7 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
if (attachments.length === 0) {
|
||||
const noAttachments = document.createElement('p');
|
||||
noAttachments.className = 'text-gray-400 text-center py-4';
|
||||
noAttachments.textContent = 'No attachments found in this PDF.';
|
||||
noAttachments.textContent = trText('No attachments found in this PDF.');
|
||||
attachmentsList.appendChild(noAttachments);
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +110,7 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
const removeAllBtn = document.createElement('button');
|
||||
removeAllBtn.className =
|
||||
'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
removeAllBtn.textContent = trText('Remove All Attachments');
|
||||
removeAllBtn.onclick = function () {
|
||||
if (pageState.allAttachments.length === 0) return;
|
||||
|
||||
@@ -131,7 +133,7 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
removeAllBtn.textContent = trText('Remove All Attachments');
|
||||
} else {
|
||||
pageState.allAttachments.forEach(function (attachment) {
|
||||
pageState.attachmentsToRemove.add(attachment.index);
|
||||
@@ -147,7 +149,7 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Deselect All';
|
||||
removeAllBtn.textContent = trText('Deselect All');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,9 +173,11 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
const levelSpan = document.createElement('span');
|
||||
levelSpan.className = 'text-gray-400 text-sm block';
|
||||
if (attachment.page === 0) {
|
||||
levelSpan.textContent = 'Document-level attachment';
|
||||
levelSpan.textContent = trText('Document-level attachment');
|
||||
} else {
|
||||
levelSpan.textContent = `Page ${attachment.page} attachment`;
|
||||
levelSpan.textContent = trText('Page {{page}} attachment', {
|
||||
page: attachment.page,
|
||||
});
|
||||
}
|
||||
|
||||
infoDiv.append(nameSpan, levelSpan);
|
||||
@@ -184,7 +188,7 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.title = 'Remove attachment';
|
||||
removeBtn.title = trText('Remove attachment');
|
||||
removeBtn.onclick = function () {
|
||||
if (pageState.attachmentsToRemove.has(attachment.index)) {
|
||||
pageState.attachmentsToRemove.delete(attachment.index);
|
||||
@@ -202,8 +206,8 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
return pageState.attachmentsToRemove.has(att.index);
|
||||
});
|
||||
removeAllBtn.textContent = allSelected
|
||||
? 'Deselect All'
|
||||
: 'Remove All Attachments';
|
||||
? trText('Deselect All')
|
||||
: trText('Remove All Attachments');
|
||||
};
|
||||
|
||||
actionsDiv.append(removeBtn);
|
||||
@@ -346,7 +350,7 @@ async function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -6,6 +6,8 @@ import { PDFName, PDFString } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: EditMetadataState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -70,14 +72,14 @@ function addCustomFieldRow(key: string = '', value: string = '') {
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key (e.g., Department)';
|
||||
keyInput.placeholder = trText('Key (e.g., Department)');
|
||||
keyInput.value = key;
|
||||
keyInput.className =
|
||||
'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||
valueInput.placeholder = trText('Value (e.g., Marketing)');
|
||||
valueInput.value = value;
|
||||
valueInput.className =
|
||||
'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||
@@ -205,7 +207,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -233,7 +237,10 @@ async function updateUI() {
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
populateMetadataFields();
|
||||
|
||||
@@ -388,7 +395,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {
|
||||
RedactionPluginLite,
|
||||
} from '@/types';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const FREETEXT_SUBTYPE = 3;
|
||||
|
||||
export function trackAppliedRedactions(
|
||||
@@ -139,7 +141,7 @@ function confirmChoice(
|
||||
cancelBtn.type = 'button';
|
||||
cancelBtn.className =
|
||||
'flex-1 bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200';
|
||||
cancelBtn.textContent = 'Cancel';
|
||||
cancelBtn.textContent = trText('Cancel');
|
||||
|
||||
const confirmBtn = document.createElement('button');
|
||||
confirmBtn.type = 'button';
|
||||
@@ -241,11 +243,7 @@ function removeFileEntry(documentId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -417,7 +415,7 @@ async function handleFiles(files: FileList) {
|
||||
downloadBtn = document.createElement('button');
|
||||
downloadBtn.id = 'download-edited-pdf';
|
||||
downloadBtn.className = 'btn-gradient w-full mt-6';
|
||||
downloadBtn.textContent = 'Download Edited PDF';
|
||||
downloadBtn.textContent = trText('Download Edited PDF');
|
||||
pdfWrapper.appendChild(downloadBtn);
|
||||
}
|
||||
downloadBtn.classList.remove('hidden');
|
||||
@@ -427,14 +425,17 @@ async function handleFiles(files: FileList) {
|
||||
const documentId = activeDocuments.getActiveDocumentId();
|
||||
const pendingCount = redactionPlugin?.getState().pendingCount ?? 0;
|
||||
if (redactionPlugin && pendingCount > 0) {
|
||||
const marks =
|
||||
pendingCount === 1
|
||||
? "1 redaction mark that hasn't"
|
||||
: `${pendingCount} redaction marks that haven't`;
|
||||
const shouldApply = await confirmChoice(
|
||||
'Apply redactions?',
|
||||
`This document has ${marks} been applied. Until a mark is applied, the content under it is still in the file. Apply all marks and download?`,
|
||||
'Apply and download'
|
||||
trText('Apply redactions?'),
|
||||
pendingCount === 1
|
||||
? trText(
|
||||
"This document has 1 redaction mark that hasn't been applied. Until a mark is applied, the content under it is still in the file. Apply all marks and download?"
|
||||
)
|
||||
: trText(
|
||||
"This document has {{count}} redaction marks that haven't been applied. Until a mark is applied, the content under it is still in the file. Apply all marks and download?",
|
||||
{ count: pendingCount }
|
||||
),
|
||||
trText('Apply and download')
|
||||
);
|
||||
if (!shouldApply) return;
|
||||
try {
|
||||
@@ -505,9 +506,11 @@ async function handleFiles(files: FileList) {
|
||||
});
|
||||
if (exported.isEncrypted) {
|
||||
const shouldDownload = await confirmChoice(
|
||||
'Encrypted file',
|
||||
"Your redactions are applied to the pages, but this file is encrypted, so BentoPDF can't also remove copies of the redacted text from its properties, bookmarks, and attachments. For a full cleanup, redact an unencrypted copy and add a password afterwards with Protect PDF. Download anyway?",
|
||||
'Download anyway'
|
||||
trText('Encrypted file'),
|
||||
trText(
|
||||
"Your redactions are applied to the pages, but this file is encrypted, so BentoPDF can't also remove copies of the redacted text from its properties, bookmarks, and attachments. For a full cleanup, redact an unencrypted copy and add a password afterwards with Protect PDF. Download anyway?"
|
||||
),
|
||||
trText('Download anyway')
|
||||
);
|
||||
if (!shouldDownload) return;
|
||||
} else {
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
type SectionSpec = { id: string; title: string; icon: string };
|
||||
type RangeLink = { range: HTMLInputElement; num: HTMLInputElement };
|
||||
|
||||
const TEXT_SECTIONS: SectionSpec[] = [
|
||||
{ id: 'font', title: 'Font', icon: 'ph-text-aa' },
|
||||
{ id: 'size', title: 'Font Size', icon: 'ph-text-t' },
|
||||
{ id: 'color', title: 'Text Color', icon: 'ph-palette' },
|
||||
{ id: 'align', title: 'Alignment', icon: 'ph-text-align-left' },
|
||||
{ id: 'spacing', title: 'Spacing', icon: 'ph-arrows-out-line-vertical' },
|
||||
{ id: 'lists', title: 'Lists', icon: 'ph-list-bullets' },
|
||||
{ id: 'more', title: 'More', icon: 'ph-dots-three' },
|
||||
const getTextSections = (): SectionSpec[] => [
|
||||
{ id: 'font', title: trText('Font'), icon: 'ph-text-aa' },
|
||||
{ id: 'size', title: trText('Font Size'), icon: 'ph-text-t' },
|
||||
{ id: 'color', title: trText('Text Color'), icon: 'ph-palette' },
|
||||
{ id: 'align', title: trText('Alignment'), icon: 'ph-text-align-left' },
|
||||
{
|
||||
id: 'spacing',
|
||||
title: trText('Spacing'),
|
||||
icon: 'ph-arrows-out-line-vertical',
|
||||
},
|
||||
{ id: 'lists', title: trText('Lists'), icon: 'ph-list-bullets' },
|
||||
{ id: 'more', title: trText('More'), icon: 'ph-dots-three' },
|
||||
];
|
||||
const SCOPE_SECTION: SectionSpec = {
|
||||
const getScopeSection = (): SectionSpec => ({
|
||||
id: 'scope',
|
||||
title: 'Edit Scope',
|
||||
title: trText('Edit Scope'),
|
||||
icon: 'ph-pencil-simple',
|
||||
};
|
||||
const STROKE_SECTION: SectionSpec = {
|
||||
});
|
||||
const getStrokeSection = (): SectionSpec => ({
|
||||
id: 'stroke',
|
||||
title: 'Path Stroke',
|
||||
title: trText('Path Stroke'),
|
||||
icon: 'ph-path',
|
||||
};
|
||||
const MULTI_SECTION: SectionSpec = {
|
||||
});
|
||||
const getMultiSection = (): SectionSpec => ({
|
||||
id: 'alignMulti',
|
||||
title: 'Align Objects',
|
||||
title: trText('Align Objects'),
|
||||
icon: 'ph-align-left',
|
||||
};
|
||||
});
|
||||
|
||||
const OBJ_BTN_IDS = [
|
||||
'rotL',
|
||||
@@ -39,16 +44,16 @@ const OBJ_BTN_IDS = [
|
||||
'dupe',
|
||||
];
|
||||
|
||||
const SLIDER_FIELDS: [string, string][] = [
|
||||
['fLine', 'Line'],
|
||||
['fPara', 'Paragraph'],
|
||||
['fChar', 'Character'],
|
||||
['fWordSp', 'Word'],
|
||||
const getSliderFields = (): [string, string][] => [
|
||||
['fLine', trText('Line')],
|
||||
['fPara', trText('Paragraph')],
|
||||
['fChar', trText('Character')],
|
||||
['fWordSp', trText('Word')],
|
||||
];
|
||||
const MORE_FIELDS: [string, string][] = [
|
||||
['fHScale', 'H-Scale %'],
|
||||
['fFirstInd', 'First Indent'],
|
||||
['fHangInd', 'Hanging Indent'],
|
||||
const getMoreFields = (): [string, string][] => [
|
||||
['fHScale', trText('H-Scale %')],
|
||||
['fFirstInd', trText('First Indent')],
|
||||
['fHangInd', trText('Hanging Indent')],
|
||||
];
|
||||
|
||||
const q = (id: string): HTMLElement | null => document.getElementById(id);
|
||||
@@ -95,7 +100,7 @@ function setupPagePill(): void {
|
||||
const pageLabel = q('pageLabel');
|
||||
const app = q('text-editor-app');
|
||||
if (!save || !pageLabel || !app || !save.parentElement) return;
|
||||
const pill = mkBtn('dpagepill', 'Go to page');
|
||||
const pill = mkBtn('dpagepill', trText('Go to page'));
|
||||
pill.textContent = '– / –';
|
||||
save.parentElement.insertBefore(pill, save);
|
||||
const syncPill = () => {
|
||||
@@ -130,10 +135,10 @@ function setupPagePill(): void {
|
||||
menu.style.right = '8px';
|
||||
menu.addEventListener('click', (e) => e.stopPropagation());
|
||||
const nav = div('dpagenav');
|
||||
const prevB = mkBtn('dnavbtn', 'Previous page');
|
||||
const prevB = mkBtn('dnavbtn', trText('Previous page'));
|
||||
prevB.appendChild(ph('ph-caret-left'));
|
||||
prevB.addEventListener('click', () => q('prev')?.click());
|
||||
const nextB = mkBtn('dnavbtn', 'Next page');
|
||||
const nextB = mkBtn('dnavbtn', trText('Next page'));
|
||||
nextB.appendChild(ph('ph-caret-right'));
|
||||
nextB.addEventListener('click', () => q('next')?.click());
|
||||
const inp = document.createElement('input');
|
||||
@@ -141,7 +146,7 @@ function setupPagePill(): void {
|
||||
inp.min = '1';
|
||||
inp.step = '1';
|
||||
inp.className = 'dpageinp';
|
||||
inp.setAttribute('aria-label', 'Page number');
|
||||
inp.setAttribute('aria-label', trText('Page number'));
|
||||
const st = parse();
|
||||
if (Number.isFinite(st.current)) inp.value = String(st.current);
|
||||
if (Number.isFinite(st.total)) inp.max = String(st.total);
|
||||
@@ -165,15 +170,15 @@ function setupPagePill(): void {
|
||||
e.preventDefault();
|
||||
go();
|
||||
});
|
||||
const goBtn = mkBtn('dnavbtn dgo', 'Go to page');
|
||||
goBtn.textContent = 'Go';
|
||||
const goBtn = mkBtn('dnavbtn dgo', trText('Go to page'));
|
||||
goBtn.textContent = trText('Go');
|
||||
goBtn.addEventListener('click', go);
|
||||
nav.append(prevB, inp, goBtn, nextB);
|
||||
menu.appendChild(nav);
|
||||
menu.appendChild(document.createElement('hr'));
|
||||
const props = mkBtn('ecmenu-item', 'Document properties');
|
||||
const props = mkBtn('ecmenu-item', trText('Document properties'));
|
||||
const propLabel = document.createElement('span');
|
||||
propLabel.textContent = 'Document properties';
|
||||
propLabel.textContent = trText('Document properties');
|
||||
props.append(ph('ph-info'), propLabel);
|
||||
props.addEventListener('click', () => {
|
||||
closeMenu();
|
||||
@@ -221,7 +226,7 @@ export function setupFormatDock(): void {
|
||||
const dticon = document.createElement('span');
|
||||
const dtext = document.createElement('span');
|
||||
dtitle.append(dticon, dtext);
|
||||
const dclose = mkBtn('dclose', 'Close panel');
|
||||
const dclose = mkBtn('dclose', trText('Close panel'));
|
||||
dclose.appendChild(ph('ph-x'));
|
||||
head.append(dtitle, dclose);
|
||||
const slotsWrap = div('dockslots');
|
||||
@@ -232,7 +237,7 @@ export function setupFormatDock(): void {
|
||||
const rail = div('dockrail');
|
||||
const chipsWrap = div('dockchips');
|
||||
const acts = div('dockacts');
|
||||
const delChip = mkBtn('dchip ddel', 'Delete');
|
||||
const delChip = mkBtn('dchip ddel', trText('Delete'));
|
||||
delChip.appendChild(ph('ph-trash'));
|
||||
delChip.addEventListener('click', () => q('del')?.click());
|
||||
acts.appendChild(delChip);
|
||||
@@ -296,8 +301,8 @@ export function setupFormatDock(): void {
|
||||
});
|
||||
return b;
|
||||
};
|
||||
const minus = step(-1, 'ph-minus', 'Decrease size');
|
||||
const plus = step(1, 'ph-plus', 'Increase size');
|
||||
const minus = step(-1, 'ph-minus', trText('Decrease size'));
|
||||
const plus = step(1, 'ph-plus', trText('Increase size'));
|
||||
const host = stepperOf(fSize) ?? fSize;
|
||||
sizer.append(minus, host, plus);
|
||||
sizeSlot.appendChild(sizer);
|
||||
@@ -311,13 +316,13 @@ export function setupFormatDock(): void {
|
||||
const alignRow = rowOf(q('fAlign'));
|
||||
if (alignRow) alignSlot.appendChild(alignRow);
|
||||
const dirLab = div('dlab dsublab');
|
||||
dirLab.textContent = 'Direction';
|
||||
dirLab.textContent = trText('Direction');
|
||||
alignSlot.appendChild(dirLab);
|
||||
const dirRow = rowOf(q('fDir'));
|
||||
if (dirRow) alignSlot.appendChild(dirRow);
|
||||
|
||||
const spacingSlot = mkSlot('spacing');
|
||||
for (const [id, label] of SLIDER_FIELDS) {
|
||||
for (const [id, label] of getSliderFields()) {
|
||||
const row = sliderRow(id, label);
|
||||
if (row) spacingSlot.appendChild(row);
|
||||
}
|
||||
@@ -328,11 +333,11 @@ export function setupFormatDock(): void {
|
||||
|
||||
const moreSlot = mkSlot('more');
|
||||
const outLab = div('dlab dsublab');
|
||||
outLab.textContent = 'Outline';
|
||||
outLab.textContent = trText('Outline');
|
||||
moreSlot.appendChild(outLab);
|
||||
const outlineRow = rowOf(q('fStrokeColor'));
|
||||
if (outlineRow) moreSlot.appendChild(outlineRow);
|
||||
for (const [id, label] of MORE_FIELDS) {
|
||||
for (const [id, label] of getMoreFields()) {
|
||||
const row = sliderRow(id, label);
|
||||
if (row) moreSlot.appendChild(row);
|
||||
}
|
||||
@@ -348,10 +353,10 @@ export function setupFormatDock(): void {
|
||||
scopeSlot.appendChild(scopeWrap);
|
||||
let picked = new Set<string>(['all']);
|
||||
const scopeDefs: [string, string][] = [
|
||||
['all', 'All'],
|
||||
['text', 'Text'],
|
||||
['image', 'Images'],
|
||||
['shape', 'Shapes'],
|
||||
['all', trText('All')],
|
||||
['text', trText('Text')],
|
||||
['image', trText('Images')],
|
||||
['shape', trText('Shapes')],
|
||||
];
|
||||
const renderScope = () => {
|
||||
scopeWrap.replaceChildren();
|
||||
@@ -459,7 +464,10 @@ export function setupFormatDock(): void {
|
||||
const grab = div('dgrab');
|
||||
grab.appendChild(document.createElement('span'));
|
||||
const shead = div('dsheethead');
|
||||
shead.append(ph('ph-text-aa'), document.createTextNode(' Select Font'));
|
||||
shead.append(
|
||||
ph('ph-text-aa'),
|
||||
document.createTextNode(` ${trText('Select Font')}`)
|
||||
);
|
||||
const fontListEl = div('dfontlist');
|
||||
sheet.append(grab, shead, fontListEl);
|
||||
inspector.append(dim, sheet);
|
||||
@@ -488,9 +496,9 @@ export function setupFormatDock(): void {
|
||||
}
|
||||
if ('queryLocalFonts' in window) {
|
||||
fontListEl.appendChild(div('ddivider'));
|
||||
const sys = mkBtn('dfrow dfsys', 'Load system fonts');
|
||||
const sys = mkBtn('dfrow dfsys', trText('Load system fonts'));
|
||||
const label = document.createElement('span');
|
||||
label.textContent = 'Load system fonts…';
|
||||
label.textContent = trText('Load system fonts…');
|
||||
sys.appendChild(label);
|
||||
sys.addEventListener('click', () => q('sysFonts')?.click());
|
||||
fontListEl.appendChild(sys);
|
||||
@@ -572,7 +580,7 @@ export function setupFormatDock(): void {
|
||||
};
|
||||
|
||||
const proxyChip = (src: HTMLButtonElement): HTMLButtonElement => {
|
||||
const b = mkBtn('dchip', src.title || 'Action');
|
||||
const b = mkBtn('dchip', src.title || trText('Action'));
|
||||
b.dataset.proxy = src.id;
|
||||
const icon = src.querySelector('i');
|
||||
b.appendChild(
|
||||
@@ -599,7 +607,7 @@ export function setupFormatDock(): void {
|
||||
|
||||
const togglesRow = q('fToggles');
|
||||
const toggleChip = (src: HTMLButtonElement): HTMLButtonElement => {
|
||||
const b = mkBtn('dchip', src.title || 'Toggle style');
|
||||
const b = mkBtn('dchip', src.title || trText('Toggle style'));
|
||||
b.dataset.tog = src.dataset.t ?? '';
|
||||
const icon = src.querySelector('i');
|
||||
b.appendChild(
|
||||
@@ -614,7 +622,7 @@ export function setupFormatDock(): void {
|
||||
chipsWrap.replaceChildren();
|
||||
if (mode !== 'idle') {
|
||||
chipsWrap.appendChild(
|
||||
toolChip('done', 'ph-check', 'Done — back to tools', () => {
|
||||
toolChip('done', 'ph-check', trText('Done: back to tools'), () => {
|
||||
closePanel();
|
||||
closeSheet();
|
||||
editBtn.click();
|
||||
@@ -622,7 +630,7 @@ export function setupFormatDock(): void {
|
||||
);
|
||||
}
|
||||
if (mode === 'text') {
|
||||
for (const s of TEXT_SECTIONS) {
|
||||
for (const s of getTextSections()) {
|
||||
chipsWrap.appendChild(secChip(s));
|
||||
if (s.id === 'color' && togglesRow) {
|
||||
for (const t of togglesRow.children) {
|
||||
@@ -633,8 +641,8 @@ export function setupFormatDock(): void {
|
||||
}
|
||||
} else if (mode === 'idle') {
|
||||
chipsWrap.appendChild(
|
||||
toolChip('edit', 'ph-pencil-simple', 'Edit tool', () => {
|
||||
if (editBtn.classList.contains('on')) openSec(SCOPE_SECTION);
|
||||
toolChip('edit', 'ph-pencil-simple', trText('Edit tool'), () => {
|
||||
if (editBtn.classList.contains('on')) openSec(getScopeSection());
|
||||
else {
|
||||
closePanel();
|
||||
applyScope();
|
||||
@@ -642,7 +650,7 @@ export function setupFormatDock(): void {
|
||||
})
|
||||
);
|
||||
chipsWrap.appendChild(
|
||||
toolChip('addText', 'ph-textbox', 'Add a text box', () => {
|
||||
toolChip('addText', 'ph-textbox', trText('Add a text box'), () => {
|
||||
closePanel();
|
||||
document
|
||||
.querySelector<HTMLButtonElement>('[data-tool="addText"]')
|
||||
@@ -650,20 +658,25 @@ export function setupFormatDock(): void {
|
||||
})
|
||||
);
|
||||
chipsWrap.appendChild(
|
||||
toolChip('addImage', 'ph-image', 'Add an image', () => {
|
||||
toolChip('addImage', 'ph-image', trText('Add an image'), () => {
|
||||
closePanel();
|
||||
q('addImage')?.click();
|
||||
})
|
||||
);
|
||||
chipsWrap.appendChild(
|
||||
toolChip('find', 'ph-magnifying-glass', 'Find & replace', () => {
|
||||
closePanel();
|
||||
q('find')?.click();
|
||||
})
|
||||
toolChip(
|
||||
'find',
|
||||
'ph-magnifying-glass',
|
||||
trText('Find & replace'),
|
||||
() => {
|
||||
closePanel();
|
||||
q('find')?.click();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
if (!alignTools.hidden) chipsWrap.appendChild(secChip(MULTI_SECTION));
|
||||
if (!pStrokeRow.hidden) chipsWrap.appendChild(secChip(STROKE_SECTION));
|
||||
if (!alignTools.hidden) chipsWrap.appendChild(secChip(getMultiSection()));
|
||||
if (!pStrokeRow.hidden) chipsWrap.appendChild(secChip(getStrokeSection()));
|
||||
for (const b of visibleObjBtns()) chipsWrap.appendChild(proxyChip(b));
|
||||
const delBtn = q('del');
|
||||
acts.hidden = !(delBtn instanceof HTMLButtonElement) || delBtn.hidden;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '@phosphor-icons/web/regular';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import {
|
||||
fallbackFontLanguages,
|
||||
@@ -64,10 +64,10 @@ function menuItem(
|
||||
return item;
|
||||
}
|
||||
|
||||
const SCOPES: [string, string, string][] = [
|
||||
['text', 'Edit Text', 'ph-text-t'],
|
||||
['image', 'Edit Images', 'ph-image'],
|
||||
['shape', 'Edit Shapes', 'ph-shapes'],
|
||||
const getScopes = (): [string, string, string][] => [
|
||||
['text', trText('Edit Text'), 'ph-text-t'],
|
||||
['image', trText('Edit Images'), 'ph-image'],
|
||||
['shape', trText('Edit Shapes'), 'ph-shapes'],
|
||||
];
|
||||
|
||||
function setupEditMenu() {
|
||||
@@ -89,12 +89,19 @@ function setupEditMenu() {
|
||||
}
|
||||
scopeSel.value = value;
|
||||
scopeSel.dispatchEvent(new Event('change'));
|
||||
const names: Record<string, string> = {
|
||||
text: trText('Text'),
|
||||
image: trText('Images'),
|
||||
shape: trText('Shapes'),
|
||||
};
|
||||
label.textContent = picked.has('all')
|
||||
? 'Edit All'
|
||||
: 'Edit ' +
|
||||
SCOPES.filter(([k]) => picked.has(k))
|
||||
.map(([, l]) => l.replace('Edit ', ''))
|
||||
.join(' + ');
|
||||
? trText('Edit All')
|
||||
: trText('Edit {{scopes}}', {
|
||||
scopes: getScopes()
|
||||
.filter(([k]) => picked.has(k))
|
||||
.map(([k]) => names[k])
|
||||
.join(' + '),
|
||||
});
|
||||
};
|
||||
let open: HTMLDivElement | null = null;
|
||||
const caret = editBtn.querySelector('.editcaret');
|
||||
@@ -121,7 +128,7 @@ function setupEditMenu() {
|
||||
menu.replaceChildren();
|
||||
menu.appendChild(
|
||||
menuItem(
|
||||
'Edit All',
|
||||
trText('Edit All'),
|
||||
picked.has('all'),
|
||||
() => {
|
||||
picked = new Set(['all']);
|
||||
@@ -131,7 +138,7 @@ function setupEditMenu() {
|
||||
'ph-selection-all'
|
||||
)
|
||||
);
|
||||
for (const [key, lbl, glyph] of SCOPES) {
|
||||
for (const [key, lbl, glyph] of getScopes()) {
|
||||
menu.appendChild(
|
||||
menuItem(
|
||||
lbl,
|
||||
@@ -140,7 +147,7 @@ function setupEditMenu() {
|
||||
if (picked.has('all')) picked.clear();
|
||||
if (picked.has(key)) picked.delete(key);
|
||||
else picked.add(key);
|
||||
if (picked.size === 0 || picked.size === SCOPES.length)
|
||||
if (picked.size === 0 || picked.size === getScopes().length)
|
||||
picked = new Set(['all']);
|
||||
apply();
|
||||
rebuild();
|
||||
@@ -232,7 +239,7 @@ function setupZoomMenu() {
|
||||
inp.type = 'number';
|
||||
inp.min = '20';
|
||||
inp.max = '600';
|
||||
inp.placeholder = 'Custom Zoom';
|
||||
inp.placeholder = trText('Custom Zoom');
|
||||
const pctSpan = document.createElement('span');
|
||||
pctSpan.textContent = '%';
|
||||
form.append(inp, pctSpan);
|
||||
@@ -299,7 +306,7 @@ function relocateAddText() {
|
||||
icon.className = 'ph ph-text-t';
|
||||
addTextBtn.appendChild(icon);
|
||||
addTextBtn.classList.add('btn', 'icon');
|
||||
addTextBtn.title = 'Add a text box';
|
||||
addTextBtn.title = trText('Add a text box');
|
||||
findBtn.parentElement.insertBefore(addTextBtn, findBtn);
|
||||
const sync = () =>
|
||||
addTextBtn.classList.toggle('on', !editBtn.classList.contains('on'));
|
||||
@@ -335,10 +342,10 @@ function setupDocPanel() {
|
||||
const head = document.createElement('div');
|
||||
head.className = 'docmodal-head';
|
||||
const title = document.createElement('span');
|
||||
title.textContent = 'Document Properties';
|
||||
title.textContent = trText('Document Properties');
|
||||
const x = document.createElement('button');
|
||||
x.className = 'btn icon';
|
||||
x.title = 'Close';
|
||||
x.title = trText('Close');
|
||||
const xi = document.createElement('i');
|
||||
xi.className = 'ph ph-x';
|
||||
x.appendChild(xi);
|
||||
@@ -349,38 +356,44 @@ function setupDocPanel() {
|
||||
const inch = (pt: number) => (pt / 72).toFixed(2);
|
||||
const groups: [string, [string, string][]][] = [
|
||||
[
|
||||
'Description',
|
||||
trText('Description'),
|
||||
[
|
||||
['File', desc.meta.name],
|
||||
['Title', f.Title || '—'],
|
||||
['Author', f.Author || '—'],
|
||||
['Subject', f.Subject || '—'],
|
||||
['Keywords', f.Keywords || '—'],
|
||||
['Created', f.CreationDate ? formatPdfDate(f.CreationDate) : '—'],
|
||||
['Modified', f.ModDate ? formatPdfDate(f.ModDate) : '—'],
|
||||
['Application', f.Creator || '—'],
|
||||
[trText('File'), desc.meta.name],
|
||||
[trText('Title'), f.Title || '—'],
|
||||
[trText('Author'), f.Author || '—'],
|
||||
[trText('Subject'), f.Subject || '—'],
|
||||
[trText('Keywords'), f.Keywords || '—'],
|
||||
[
|
||||
trText('Created'),
|
||||
f.CreationDate ? formatPdfDate(f.CreationDate) : '—',
|
||||
],
|
||||
[trText('Modified'), f.ModDate ? formatPdfDate(f.ModDate) : '—'],
|
||||
[trText('Application'), f.Creator || '—'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'Advanced',
|
||||
trText('Advanced'),
|
||||
[
|
||||
['PDF Producer', f.Producer || '—'],
|
||||
['File Size', `${(desc.meta.size / 1024).toFixed(0)} KB`],
|
||||
[trText('PDF Producer'), f.Producer || '—'],
|
||||
[trText('File Size'), `${(desc.meta.size / 1024).toFixed(0)} KB`],
|
||||
[
|
||||
'Page Size',
|
||||
trText('Page Size'),
|
||||
desc.pageSize
|
||||
? `${inch(desc.pageSize.w)} × ${inch(desc.pageSize.h)} in`
|
||||
: '—',
|
||||
],
|
||||
['Number of Pages', desc.info?.pages ? String(desc.info.pages) : '—'],
|
||||
[
|
||||
trText('Number of Pages'),
|
||||
desc.info?.pages ? String(desc.info.pages) : '—',
|
||||
],
|
||||
...(desc.info?.signatures
|
||||
? ([['Signatures', String(desc.info.signatures)]] as [
|
||||
? ([[trText('Signatures'), String(desc.info.signatures)]] as [
|
||||
string,
|
||||
string,
|
||||
][])
|
||||
: []),
|
||||
...(desc.pdfa
|
||||
? ([['Conformance', 'PDF/A']] as [string, string][])
|
||||
? ([[trText('Conformance'), 'PDF/A']] as [string, string][])
|
||||
: []),
|
||||
],
|
||||
],
|
||||
@@ -819,7 +832,7 @@ function initializePage() {
|
||||
if (document.activeElement !== pageInput)
|
||||
pageInput.value = String(current);
|
||||
pageInput.max = String(total);
|
||||
pageTotal.textContent = `/ ${total}`;
|
||||
pageTotal.textContent = trText('/ {{total}}', { total });
|
||||
};
|
||||
new MutationObserver(sync).observe(pageLabel, {
|
||||
childList: true,
|
||||
@@ -1045,8 +1058,4 @@ function initializePage() {
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
@@ -5,10 +5,12 @@ import { createIcons, icons } from 'lucide';
|
||||
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const EXTENSIONS = ['.eml', '.msg'];
|
||||
const TOOL_NAME = 'Email';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -87,7 +89,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert(
|
||||
'No Files',
|
||||
`Please select at least one ${TOOL_NAME} file (.eml or .msg).`
|
||||
trText(
|
||||
'Please select at least one {{TOOL_NAME}} file (.eml or .msg).',
|
||||
{ TOOL_NAME }
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -112,7 +117,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Parsing ${originalFile.name}...`);
|
||||
showLoader(trText('Parsing {{name}}...', { name: originalFile.name }));
|
||||
|
||||
const email = await parseEmailFile(originalFile);
|
||||
|
||||
@@ -144,7 +149,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -156,7 +163,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -197,7 +208,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
trText(
|
||||
'Successfully converted {{length}} {{TOOL_NAME}} file(s) to PDF.',
|
||||
{ length: state.files.length, TOOL_NAME }
|
||||
),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -207,7 +221,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '../utils/helpers.js';
|
||||
import type { EmailAttachment, ParsedEmail, EmailRenderOptions } from '@/types';
|
||||
|
||||
import { trText } from '../i18n/i18n.js';
|
||||
|
||||
type MsgReaderCtor = typeof MsgReaderDefault;
|
||||
const MsgReader: MsgReaderCtor =
|
||||
(MsgReaderDefault as unknown as { default?: MsgReaderCtor }).default ??
|
||||
@@ -333,6 +335,6 @@ export async function parseEmailFile(file: File): Promise<ParsedEmail> {
|
||||
} else if (ext === 'msg') {
|
||||
return parseMsgFile(file);
|
||||
} else {
|
||||
throw new Error(`Unsupported file type: .${ext}`);
|
||||
throw new Error(trText('Unsupported file type: .{{ext}}', { ext }));
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const FILETYPE = 'epub';
|
||||
const EXTENSIONS = ['.epub'];
|
||||
const TOOL_NAME = 'EPUB';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -87,7 +89,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{TOOL_NAME}} file.', {
|
||||
TOOL_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,7 +103,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
showLoader(
|
||||
trText('Converting {{name}}...', { name: originalFile.name })
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||
filetype: FILETYPE,
|
||||
@@ -108,7 +117,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -120,7 +131,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||
@@ -138,7 +153,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
trText(
|
||||
'Successfully converted {{length}} {{TOOL_NAME}} file(s) to PDF.',
|
||||
{ length: state.files.length, TOOL_NAME }
|
||||
),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -148,7 +166,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
type LoadProgress,
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -114,7 +116,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -126,7 +130,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
@@ -144,7 +152,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} Excel file(s) to PDF.`,
|
||||
trText('Successfully converted {{length}} Excel file(s) to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -153,7 +163,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
} from '../utils/wasm-provider.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const worker = new Worker(
|
||||
import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'
|
||||
);
|
||||
@@ -114,18 +116,24 @@ worker.onmessage = function (e) {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`${attachments.length} attachment(s) extracted successfully!`
|
||||
trText('{{length}} attachment(s) extracted successfully!', {
|
||||
length: attachments.length,
|
||||
})
|
||||
);
|
||||
|
||||
showStatus(
|
||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||
trText(
|
||||
'Extraction completed! {{length}} attachment(s) in zip file ({{totalSize}}). Download started.',
|
||||
{ length: attachments.length, totalSize: formatBytes(totalSize) }
|
||||
),
|
||||
'success'
|
||||
);
|
||||
|
||||
resetState();
|
||||
});
|
||||
} else if (e.data.status === 'error') {
|
||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||
const errorMessage =
|
||||
e.data.message || trText('Unknown error occurred in worker.');
|
||||
console.error('Worker Error:', errorMessage);
|
||||
|
||||
if (errorMessage.includes('No attachments were found')) {
|
||||
@@ -142,7 +150,10 @@ worker.onmessage = function (e) {
|
||||
|
||||
worker.onerror = function (error) {
|
||||
console.error('Worker error:', error);
|
||||
showStatus('Worker error occurred. Check console for details.', 'error');
|
||||
showStatus(
|
||||
trText('Worker error occurred. Check console for details.'),
|
||||
'error'
|
||||
);
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
@@ -169,7 +180,9 @@ async function updateUI() {
|
||||
|
||||
const countSpan = document.createElement('div');
|
||||
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
|
||||
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
|
||||
countSpan.textContent = trText('{{length}} PDF file(s) selected', {
|
||||
length: pageState.files.length,
|
||||
});
|
||||
|
||||
const sizeSpan = document.createElement('div');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
@@ -199,7 +212,7 @@ async function updateUI() {
|
||||
|
||||
async function extractAttachments() {
|
||||
if (pageState.files.length === 0) {
|
||||
showStatus('No Files', 'error');
|
||||
showStatus(trText('No Files'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,7 +228,7 @@ async function extractAttachments() {
|
||||
processBtn.setAttribute('disabled', 'true');
|
||||
}
|
||||
|
||||
showStatus('Reading files...', 'info');
|
||||
showStatus(trText('Reading files...'), 'info');
|
||||
|
||||
try {
|
||||
pageState.files = await batchDecryptIfNeeded(pageState.files);
|
||||
@@ -239,7 +252,9 @@ async function extractAttachments() {
|
||||
}
|
||||
|
||||
showStatus(
|
||||
`Extracting attachments from ${pageState.files.length} file(s)...`,
|
||||
trText('Extracting attachments from {{length}} file(s)...', {
|
||||
length: pageState.files.length,
|
||||
}),
|
||||
'info'
|
||||
);
|
||||
|
||||
@@ -257,7 +272,10 @@ async function extractAttachments() {
|
||||
} catch (error) {
|
||||
console.error('Error reading files:', error);
|
||||
showStatus(
|
||||
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
||||
trText('Error reading files: {{error}}', {
|
||||
error:
|
||||
error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
}),
|
||||
'error'
|
||||
);
|
||||
|
||||
@@ -282,7 +300,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
@@ -19,7 +19,7 @@ interface ExtractedImage {
|
||||
|
||||
let extractedImages: ExtractedImage[] = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -66,7 +66,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -85,9 +88,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} pages', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdfDoc.numPages,
|
||||
});
|
||||
} catch {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
metaSpan.textContent = trText(
|
||||
'{{size}} • Could not load page count',
|
||||
{ size: formatBytes(file.size) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +176,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Extracting images from ${file.name}...`);
|
||||
showLoader(
|
||||
trText('Extracting images from {{name}}...', { name: file.name })
|
||||
);
|
||||
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
@@ -206,7 +217,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
displayImages();
|
||||
showAlert(
|
||||
'Extraction Complete',
|
||||
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
|
||||
trText('Found {{length}} image(s) in {{length2}} PDF(s).', {
|
||||
length: extractedImages.length,
|
||||
length2: state.files.length,
|
||||
}),
|
||||
'success'
|
||||
);
|
||||
}
|
||||
@@ -214,7 +228,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during extraction. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during extraction. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import JSZip from 'jszip';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface ExtractState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFDocument | null;
|
||||
@@ -22,11 +24,7 @@ const extractState: ExtractState = {
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -131,7 +129,10 @@ function updateFileDisplay() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{totalPages}} pages', {
|
||||
size: formatBytes(extractState.file.size),
|
||||
totalPages: extractState.totalPages,
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -197,7 +198,9 @@ async function extractPages() {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
`Extracted ${pagesToExtract.length} page(s) successfully!`,
|
||||
trText('Extracted {{length}} page(s) successfully!', {
|
||||
length: pagesToExtract.length,
|
||||
}),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
@@ -117,7 +119,12 @@ async function extract() {
|
||||
const allTables: TableData[] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
showLoader(
|
||||
trText('Scanning page {{n}} of {{pageCount}}...', {
|
||||
n: i + 1,
|
||||
pageCount,
|
||||
})
|
||||
);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
@@ -193,21 +200,26 @@ async function extract() {
|
||||
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
||||
showAlert(
|
||||
'Success',
|
||||
`Extracted ${allTables.length} tables successfully!`,
|
||||
trText('Extracted {{length}} tables successfully!', {
|
||||
length: allTables.length,
|
||||
}),
|
||||
'success',
|
||||
resetState
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to extract tables. ${message}`);
|
||||
const message = e instanceof Error ? e.message : trText('Unknown error');
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('Failed to extract tables. {{message}}', { message })
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -4,11 +4,13 @@ import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const FILETYPE = 'fb2';
|
||||
const EXTENSIONS = ['.fb2'];
|
||||
const TOOL_NAME = 'FB2';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -83,7 +85,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{TOOL_NAME}} file.', {
|
||||
TOOL_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +99,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
showLoader(
|
||||
trText('Converting {{name}}...', { name: originalFile.name })
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||
filetype: FILETYPE,
|
||||
@@ -104,7 +113,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -116,7 +127,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||
@@ -134,7 +149,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
trText(
|
||||
'Successfully converted {{length}} {{TOOL_NAME}} file(s) to PDF.',
|
||||
{ length: state.files.length, TOOL_NAME }
|
||||
),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -144,7 +162,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import { FixPageSizeState } from '@/types';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: FixPageSizeState = {
|
||||
file: null,
|
||||
};
|
||||
@@ -105,7 +107,7 @@ async function fixPageSize() {
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Standardizing pages...';
|
||||
if (loaderText) loaderText.textContent = trText('Standardizing pages...');
|
||||
|
||||
try {
|
||||
const customWidth =
|
||||
@@ -153,7 +155,7 @@ async function fixPageSize() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
removeUnreferencedObjects,
|
||||
} from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: FlattenPdfState = {
|
||||
files: [],
|
||||
};
|
||||
@@ -112,7 +114,7 @@ async function flattenPdf() {
|
||||
try {
|
||||
if (pageState.files.length === 1) {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
||||
if (loaderText) loaderText.textContent = trText('Flattening PDF...');
|
||||
|
||||
const file = pageState.files[0];
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
@@ -142,7 +144,8 @@ async function flattenPdf() {
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
} else {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
|
||||
if (loaderText)
|
||||
loaderText.textContent = trText('Flattening multiple PDFs...');
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
@@ -151,7 +154,10 @@ async function flattenPdf() {
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||
loaderText.textContent = trText(
|
||||
'Flattening {{n}}/{{length}}: {{name}}...',
|
||||
{ n: i + 1, length: pageState.files.length, name: file.name }
|
||||
);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
@@ -189,7 +195,7 @@ async function flattenPdf() {
|
||||
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||
showAlert(
|
||||
'Success',
|
||||
`Processed ${processedCount} PDFs.`,
|
||||
trText('Processed {{processedCount}} PDFs.', { processedCount }),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
@@ -204,12 +210,12 @@ async function flattenPdf() {
|
||||
console.error(e);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'An unexpected error occurred.';
|
||||
e instanceof Error ? e.message : trText('An unexpected error occurred.');
|
||||
showAlert('Error', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -8,6 +8,8 @@ import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface FontToOutlineState {
|
||||
files: File[];
|
||||
}
|
||||
@@ -117,7 +119,7 @@ async function processFiles() {
|
||||
if (pageState.files.length === 1) {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText)
|
||||
loaderText.textContent = 'Converting fonts to outlines...';
|
||||
loaderText.textContent = trText('Converting fonts to outlines...');
|
||||
|
||||
const file = pageState.files[0];
|
||||
const resultBlob = await convertFileToOutlines(file, (msg) => {
|
||||
@@ -128,7 +130,8 @@ async function processFiles() {
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
} else {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Processing multiple PDFs...';
|
||||
if (loaderText)
|
||||
loaderText.textContent = trText('Processing multiple PDFs...');
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
@@ -137,7 +140,10 @@ async function processFiles() {
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Processing ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||
loaderText.textContent = trText(
|
||||
'Processing {{n}}/{{length}}: {{name}}...',
|
||||
{ n: i + 1, length: pageState.files.length, name: file.name }
|
||||
);
|
||||
|
||||
try {
|
||||
const resultBlob = await convertFileToOutlines(file, () => {});
|
||||
@@ -155,7 +161,7 @@ async function processFiles() {
|
||||
downloadFile(zipBlob, 'outlined_pdfs.zip');
|
||||
showAlert(
|
||||
'Success',
|
||||
`Processed ${processedCount} PDFs.`,
|
||||
trText('Processed {{processedCount}} PDFs.', { processedCount }),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
@@ -170,12 +176,12 @@ async function processFiles() {
|
||||
console.error(e);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
const errorMessage =
|
||||
e instanceof Error ? e.message : 'An unexpected error occurred.';
|
||||
e instanceof Error ? e.message : trText('An unexpected error occurred.');
|
||||
showAlert('Error', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
+138
-104
@@ -41,6 +41,8 @@ import { extractExistingFields as extractExistingPdfFields } from './form-creato
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, trHtml } from '../i18n/i18n.js';
|
||||
|
||||
let fields: FormField[] = [];
|
||||
let selectedField: FormField | null = null;
|
||||
let fieldCounter = 0;
|
||||
@@ -247,7 +249,11 @@ if (gotoPageBtn && gotoPageInput) {
|
||||
renderCanvas();
|
||||
updatePageNavigation();
|
||||
} else {
|
||||
alert(`Please enter a valid page number between 1 and ${pages.length}`);
|
||||
alert(
|
||||
trText('Please enter a valid page number between 1 and {{total}}', {
|
||||
total: pages.length,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -389,6 +395,13 @@ canvas.addEventListener('click', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
const standardFontText = (text: string, fallback: string): string =>
|
||||
/^[\u0020-\u007e\u00a0-\u00ff\u2013\u2014\u2018\u2019\u201c\u201d\u2026\u20ac]*$/.test(
|
||||
text
|
||||
)
|
||||
? text
|
||||
: fallback;
|
||||
|
||||
function createField(type: FormField['type'], x: number, y: number): void {
|
||||
fieldCounter++;
|
||||
const field: FormField = {
|
||||
@@ -429,7 +442,10 @@ function createField(type: FormField['type'], x: number, y: number): void {
|
||||
type === 'button'
|
||||
? 'Button'
|
||||
: type === 'image'
|
||||
? 'Click to Upload Image'
|
||||
? standardFontText(
|
||||
trText('Click to Upload Image'),
|
||||
'Click to Upload Image'
|
||||
)
|
||||
: undefined,
|
||||
action: type === 'button' ? 'none' : undefined,
|
||||
jsScript: type === 'button' ? 'app.alert("Hello World!");' : undefined,
|
||||
@@ -683,7 +699,7 @@ function renderField(field: FormField): void {
|
||||
// Empty state
|
||||
const optEl = document.createElement('div');
|
||||
optEl.className = 'px-1 w-full text-black italic';
|
||||
optEl.textContent = 'Item 1';
|
||||
optEl.textContent = trText('Item 1');
|
||||
contentEl.appendChild(optEl);
|
||||
}
|
||||
} else if (field.type === 'button') {
|
||||
@@ -694,7 +710,7 @@ function renderField(field: FormField): void {
|
||||
'#e5e7eb'
|
||||
);
|
||||
contentEl.style.color = field.textColor || '#000000';
|
||||
contentEl.textContent = field.label || 'Button';
|
||||
contentEl.textContent = field.label || trText('Button');
|
||||
} else if (field.type === 'signature') {
|
||||
contentEl.className =
|
||||
'w-full h-full flex items-center justify-center text-gray-400';
|
||||
@@ -702,8 +718,7 @@ function renderField(field: FormField): void {
|
||||
field,
|
||||
'#f9fafb'
|
||||
);
|
||||
contentEl.innerHTML =
|
||||
'<div class="flex flex-col items-center"><i data-lucide="pen-tool" class="w-6 h-6 mb-1"></i><span class="text-[10px]">Sign Here</span></div>';
|
||||
contentEl.innerHTML = `<div class="flex flex-col items-center"><i data-lucide="pen-tool" class="w-6 h-6 mb-1"></i><span class="text-[10px]">${trHtml('Sign Here')}</span></div>`;
|
||||
setTimeout(() => (window as LucideWindow).lucide?.createIcons(), 0);
|
||||
} else if (field.type === 'date') {
|
||||
contentEl.className =
|
||||
@@ -750,11 +765,11 @@ function renderField(field: FormField): void {
|
||||
String(field.name).replace(/[\r\n]+/g, ' '),
|
||||
error
|
||||
);
|
||||
contentEl.innerHTML = `<div class="flex flex-col items-center text-center p-1 text-gray-400"><i data-lucide="qr-code" class="w-6 h-6 mb-1"></i><span class="text-[10px] leading-tight">Invalid data</span></div>`;
|
||||
contentEl.innerHTML = `<div class="flex flex-col items-center text-center p-1 text-gray-400"><i data-lucide="qr-code" class="w-6 h-6 mb-1"></i><span class="text-[10px] leading-tight">${trHtml('Invalid data')}</span></div>`;
|
||||
setTimeout(() => (window as LucideWindow).lucide?.createIcons(), 0);
|
||||
}
|
||||
} else {
|
||||
contentEl.innerHTML = `<div class="flex flex-col items-center text-center p-1 text-gray-400"><i data-lucide="qr-code" class="w-6 h-6 mb-1"></i><span class="text-[10px] leading-tight">Barcode</span></div>`;
|
||||
contentEl.innerHTML = `<div class="flex flex-col items-center text-center p-1 text-gray-400"><i data-lucide="qr-code" class="w-6 h-6 mb-1"></i><span class="text-[10px] leading-tight">${trHtml('Barcode')}</span></div>`;
|
||||
setTimeout(() => (window as LucideWindow).lucide?.createIcons(), 0);
|
||||
}
|
||||
}
|
||||
@@ -1090,35 +1105,35 @@ function showProperties(field: FormField): void {
|
||||
if (field.type === 'text') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Value</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Value')}</label>
|
||||
<input type="text" id="propValue" value="${escapeHtml(field.defaultValue)}" ${field.combCells > 0 ? `maxlength="${field.combCells}"` : field.maxLength > 0 ? `maxlength="${field.maxLength}"` : ''} class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Max Length (0 for unlimited)</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Max Length (0 for unlimited)')}</label>
|
||||
<input type="number" id="propMaxLength" value="${field.maxLength}" min="0" ${field.combCells > 0 ? 'disabled' : ''} class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500 disabled:opacity-50">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Divide into boxes (0 to disable)</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Divide into boxes (0 to disable)')}</label>
|
||||
<input type="number" id="propComb" value="${field.combCells}" min="0" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Font Size</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Font Size')}</label>
|
||||
<input type="number" id="propFontSize" value="${field.fontSize}" min="8" max="72" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Text Color</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Text Color')}</label>
|
||||
<input type="color" id="propTextColor" value="${field.textColor}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Alignment</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Alignment')}</label>
|
||||
<select id="propAlignment" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="left" ${field.alignment === 'left' ? 'selected' : ''}>Left</option>
|
||||
<option value="center" ${field.alignment === 'center' ? 'selected' : ''}>Center</option>
|
||||
<option value="right" ${field.alignment === 'right' ? 'selected' : ''}>Right</option>
|
||||
<option value="left" ${field.alignment === 'left' ? 'selected' : ''}>${trHtml('Left')}</option>
|
||||
<option value="center" ${field.alignment === 'center' ? 'selected' : ''}>${trHtml('Center')}</option>
|
||||
<option value="right" ${field.alignment === 'right' ? 'selected' : ''}>${trHtml('Right')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-gray-600 p-2 rounded mt-2">
|
||||
<label for="propMultiline" class="text-xs font-semibold text-gray-300">Multi-line</label>
|
||||
<label for="propMultiline" class="text-xs font-semibold text-gray-300">${trHtml('Multi-line')}</label>
|
||||
<button id="propMultilineBtn" class="w-12 h-6 rounded-full transition-colors duration-200 focus:outline-none ${field.multiline ? 'bg-indigo-600' : 'bg-gray-500'} relative">
|
||||
<span class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ${field.multiline ? 'translate-x-6' : 'translate-x-0'}"></span>
|
||||
</button>
|
||||
@@ -1127,7 +1142,7 @@ function showProperties(field: FormField): void {
|
||||
} else if (field.type === 'checkbox') {
|
||||
specificProps = `
|
||||
<div class="flex items-center justify-between bg-gray-600 p-2 rounded">
|
||||
<label for="propChecked" class="text-xs font-semibold text-gray-300">Checked State</label>
|
||||
<label for="propChecked" class="text-xs font-semibold text-gray-300">${trHtml('Checked State')}</label>
|
||||
<button id="propCheckedBtn" class="w-12 h-6 rounded-full transition-colors duration-200 focus:outline-none ${field.checked ? 'bg-indigo-600' : 'bg-gray-500'} relative">
|
||||
<span class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ${field.checked ? 'translate-x-6' : 'translate-x-0'}"></span>
|
||||
</button>
|
||||
@@ -1136,15 +1151,15 @@ function showProperties(field: FormField): void {
|
||||
} else if (field.type === 'radio') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Group Name (Must be same for group)</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Group Name (Must be same for group)')}</label>
|
||||
<input type="text" id="propGroupName" value="${escapeHtml(field.groupName)}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Export Value</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Export Value')}</label>
|
||||
<input type="text" id="propExportValue" value="${escapeHtml(field.exportValue)}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-gray-600 p-2 rounded mt-2">
|
||||
<label for="propChecked" class="text-xs font-semibold text-gray-300">Checked State</label>
|
||||
<label for="propChecked" class="text-xs font-semibold text-gray-300">${trHtml('Checked State')}</label>
|
||||
<button id="propCheckedBtn" class="w-12 h-6 rounded-full transition-colors duration-200 focus:outline-none ${field.checked ? 'bg-indigo-600' : 'bg-gray-500'} relative">
|
||||
<span class="absolute top-1 left-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ${field.checked ? 'translate-x-6' : 'translate-x-0'}"></span>
|
||||
</button>
|
||||
@@ -1153,50 +1168,50 @@ function showProperties(field: FormField): void {
|
||||
} else if (field.type === 'dropdown' || field.type === 'optionlist') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Options (One per line or comma separated)</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Options (One per line or comma separated)')}</label>
|
||||
<textarea id="propOptions" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500 h-24">${escapeHtml(field.options?.join('\n') ?? '')}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Selected Option</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Selected Option')}</label>
|
||||
<select id="propSelectedOption" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">None</option>
|
||||
<option value="">${trHtml('None')}</option>
|
||||
${field.options?.map((opt) => `<option value="${escapeHtml(opt)}" ${field.defaultValue === opt ? 'selected' : ''}>${escapeHtml(opt)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 italic mt-2">
|
||||
To actually fill or change the options, use our PDF Form Filler tool.
|
||||
${trHtml('To actually fill or change the options, use our PDF Form Filler tool.')}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'button') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Label</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Label')}</label>
|
||||
<input type="text" id="propLabel" value="${escapeHtml(field.label)}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Action</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Action')}</label>
|
||||
<select id="propAction" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="none" ${field.action === 'none' ? 'selected' : ''}>None</option>
|
||||
<option value="reset" ${field.action === 'reset' ? 'selected' : ''}>Reset Form</option>
|
||||
<option value="print" ${field.action === 'print' ? 'selected' : ''}>Print Form</option>
|
||||
<option value="url" ${field.action === 'url' ? 'selected' : ''}>Open URL</option>
|
||||
<option value="js" ${field.action === 'js' ? 'selected' : ''}>Run Javascript</option>
|
||||
<option value="showHide" ${field.action === 'showHide' ? 'selected' : ''}>Show/Hide Field</option>
|
||||
<option value="none" ${field.action === 'none' ? 'selected' : ''}>${trHtml('None')}</option>
|
||||
<option value="reset" ${field.action === 'reset' ? 'selected' : ''}>${trHtml('Reset Form')}</option>
|
||||
<option value="print" ${field.action === 'print' ? 'selected' : ''}>${trHtml('Print Form')}</option>
|
||||
<option value="url" ${field.action === 'url' ? 'selected' : ''}>${trHtml('Open URL')}</option>
|
||||
<option value="js" ${field.action === 'js' ? 'selected' : ''}>${trHtml('Run Javascript')}</option>
|
||||
<option value="showHide" ${field.action === 'showHide' ? 'selected' : ''}>${trHtml('Show/Hide Field')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="propUrlContainer" class="${field.action === 'url' ? '' : 'hidden'}">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">URL</label>
|
||||
<input type="text" id="propActionUrl" value="${escapeHtml(field.actionUrl || '')}" placeholder="https://example.com" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('URL')}</label>
|
||||
<input type="text" id="propActionUrl" value="${escapeHtml(field.actionUrl || '')}" placeholder="${trHtml('https://example.com')}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div id="propJsContainer" class="${field.action === 'js' ? '' : 'hidden'}">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Javascript Code</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Javascript Code')}</label>
|
||||
<textarea id="propJsScript" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500 h-24 font-mono">${escapeHtml(field.jsScript || '')}</textarea>
|
||||
</div>
|
||||
<div id="propShowHideContainer" class="${field.action === 'showHide' ? '' : 'hidden'}">
|
||||
<div class="mb-2">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Target Field</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Target Field')}</label>
|
||||
<select id="propTargetField" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">Select a field...</option>
|
||||
<option value="">${trHtml('Select a field...')}</option>
|
||||
${fields
|
||||
.filter((f) => f.id !== field.id)
|
||||
.map(
|
||||
@@ -1207,11 +1222,11 @@ function showProperties(field: FormField): void {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Visibility</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Visibility')}</label>
|
||||
<select id="propVisibilityAction" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="show" ${field.visibilityAction === 'show' ? 'selected' : ''}>Show</option>
|
||||
<option value="hide" ${field.visibilityAction === 'hide' ? 'selected' : ''}>Hide</option>
|
||||
<option value="toggle" ${field.visibilityAction === 'toggle' ? 'selected' : ''}>Toggle</option>
|
||||
<option value="show" ${field.visibilityAction === 'show' ? 'selected' : ''}>${trHtml('Show')}</option>
|
||||
<option value="hide" ${field.visibilityAction === 'hide' ? 'selected' : ''}>${trHtml('Hide')}</option>
|
||||
<option value="toggle" ${field.visibilityAction === 'toggle' ? 'selected' : ''}>${trHtml('Toggle')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1219,7 +1234,7 @@ function showProperties(field: FormField): void {
|
||||
} else if (field.type === 'signature') {
|
||||
specificProps = `
|
||||
<div class="text-xs text-gray-400 italic mb-2">
|
||||
Signature fields are AcroForm signature fields and would only be visible in an advanced PDF viewer.
|
||||
${trHtml('Signature fields are AcroForm signature fields and would only be visible in an advanced PDF viewer.')}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'date') {
|
||||
@@ -1259,53 +1274,53 @@ function showProperties(field: FormField): void {
|
||||
const isCustom = !formats.includes(field.dateFormat || 'mm/dd/yyyy');
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Date Format</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Date Format')}</label>
|
||||
<select id="propDateFormat" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
${formats.map((f) => `<option value="${f}" ${field.dateFormat === f ? 'selected' : ''}>${f}</option>`).join('')}
|
||||
<option value="custom" ${isCustom ? 'selected' : ''}>Custom</option>
|
||||
<option value="custom" ${isCustom ? 'selected' : ''}>${trHtml('Custom')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="customFormatContainer" class="${isCustom ? '' : 'hidden'} mt-2">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Custom Format</label>
|
||||
<input type="text" id="propCustomFormat" value="${isCustom ? escapeHtml(field.dateFormat ?? '') : ''}" placeholder="e.g. dd/mm/yyyy HH:MM:ss" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Custom Format')}</label>
|
||||
<input type="text" id="propCustomFormat" value="${isCustom ? escapeHtml(field.dateFormat ?? '') : ''}" placeholder="${trHtml('e.g. dd/mm/yyyy HH:MM:ss')}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div class="mt-3 p-2 bg-gray-700 rounded">
|
||||
<span class="text-xs text-gray-400">Example of current format:</span>
|
||||
<span class="text-xs text-gray-400">${trHtml('Example of current format:')}</span>
|
||||
<span id="dateFormatExample" class="text-sm text-white font-medium ml-2"></span>
|
||||
</div>
|
||||
<div class="bg-blue-900/30 border border-blue-700/50 rounded p-2 mt-2">
|
||||
<p class="text-xs text-blue-200">
|
||||
<i data-lucide="info" class="w-4 h-4 flex-shrink-0 mt-0.5"></i>
|
||||
<span><strong>Browser Note:</strong> Firefox and Chrome may show their native date picker format during selection. The correct format will apply when you finish entering the date.</span>
|
||||
<span><strong>${trHtml('Browser Note:')}</strong> ${trHtml('Firefox and Chrome may show their native date picker format during selection. The correct format will apply when you finish entering the date.')}</span>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'image') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Label / Prompt</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Label / Prompt')}</label>
|
||||
<input type="text" id="propLabel" value="${escapeHtml(field.label)}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 italic mt-2">
|
||||
Clicking this field in the PDF will open a file picker to upload an image.
|
||||
${trHtml('Clicking this field in the PDF will open a file picker to upload an image.')}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'barcode') {
|
||||
specificProps = `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Barcode Format</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Barcode Format')}</label>
|
||||
<select id="propBarcodeFormat" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="qrcode" ${field.barcodeFormat === 'qrcode' ? 'selected' : ''}>QR Code</option>
|
||||
<option value="code128" ${field.barcodeFormat === 'code128' ? 'selected' : ''}>Code 128</option>
|
||||
<option value="code39" ${field.barcodeFormat === 'code39' ? 'selected' : ''}>Code 39</option>
|
||||
<option value="ean13" ${field.barcodeFormat === 'ean13' ? 'selected' : ''}>EAN-13</option>
|
||||
<option value="upca" ${field.barcodeFormat === 'upca' ? 'selected' : ''}>UPC-A</option>
|
||||
<option value="datamatrix" ${field.barcodeFormat === 'datamatrix' ? 'selected' : ''}>DataMatrix</option>
|
||||
<option value="pdf417" ${field.barcodeFormat === 'pdf417' ? 'selected' : ''}>PDF417</option>
|
||||
<option value="qrcode" ${field.barcodeFormat === 'qrcode' ? 'selected' : ''}>${trHtml('QR Code')}</option>
|
||||
<option value="code128" ${field.barcodeFormat === 'code128' ? 'selected' : ''}>${trHtml('Code 128')}</option>
|
||||
<option value="code39" ${field.barcodeFormat === 'code39' ? 'selected' : ''}>${trHtml('Code 39')}</option>
|
||||
<option value="ean13" ${field.barcodeFormat === 'ean13' ? 'selected' : ''}>${trHtml('EAN-13')}</option>
|
||||
<option value="upca" ${field.barcodeFormat === 'upca' ? 'selected' : ''}>${trHtml('UPC-A')}</option>
|
||||
<option value="datamatrix" ${field.barcodeFormat === 'datamatrix' ? 'selected' : ''}>${trHtml('DataMatrix')}</option>
|
||||
<option value="pdf417" ${field.barcodeFormat === 'pdf417' ? 'selected' : ''}>${trHtml('PDF417')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Barcode Value</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Barcode Value')}</label>
|
||||
<input type="text" id="propBarcodeValue" value="${escapeHtml(field.barcodeValue || '')}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div id="barcodeFormatHint" class="text-xs text-gray-400 italic"></div>
|
||||
@@ -1315,7 +1330,7 @@ function showProperties(field: FormField): void {
|
||||
const propertiesHtml = `
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${field.type === 'radio' ? trHtml('Field Name (Group Name)') : trHtml('Field Name')}</label>
|
||||
<input type="text" id="propName" value="${escapeHtml(field.name)}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<div id="nameError" class="hidden text-red-400 text-xs mt-1"></div>
|
||||
</div>
|
||||
@@ -1325,9 +1340,9 @@ function showProperties(field: FormField): void {
|
||||
fields.some((f) => f.type === 'radio' && f.id !== field.id))
|
||||
? `
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Existing Radio Groups</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Existing Radio Groups')}</label>
|
||||
<select id="existingGroups" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">-- Select existing group --</option>
|
||||
<option value="">${trHtml('-- Select existing group --')}</option>
|
||||
${Array.from(existingRadioGroups)
|
||||
.map(
|
||||
(name) =>
|
||||
@@ -1348,38 +1363,38 @@ function showProperties(field: FormField): void {
|
||||
)
|
||||
.join('')}
|
||||
</select>
|
||||
<p class="text-xs text-gray-400 mt-1">Select to add this button to an existing group</p>
|
||||
<p class="text-xs text-gray-400 mt-1">${trHtml('Select to add this button to an existing group')}</p>
|
||||
</div>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${specificProps}
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Tooltip / Help Text</label>
|
||||
<input type="text" id="propTooltip" value="${escapeHtml(field.tooltip)}" placeholder="Description for screen readers" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Tooltip / Help Text')}</label>
|
||||
<input type="text" id="propTooltip" value="${escapeHtml(field.tooltip)}" placeholder="${trHtml('Description for screen readers')}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="propRequired" ${field.required ? 'checked' : ''} class="mr-2">
|
||||
<label for="propRequired" class="text-xs font-semibold text-gray-300">Required</label>
|
||||
<label for="propRequired" class="text-xs font-semibold text-gray-300">${trHtml('Required')}</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="propReadOnly" ${field.readOnly ? 'checked' : ''} class="mr-2">
|
||||
<label for="propReadOnly" class="text-xs font-semibold text-gray-300">Read Only</label>
|
||||
<label for="propReadOnly" class="text-xs font-semibold text-gray-300">${trHtml('Read Only')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Border Color</label>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">${trHtml('Border Color')}</label>
|
||||
<input type="color" id="propBorderColor" value="${field.borderColor || '#000000'}">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="propHideBorder" ${field.hideBorder ? 'checked' : ''} class="mr-2">
|
||||
<label for="propHideBorder" class="text-xs font-semibold text-gray-300">Hide Border</label>
|
||||
<label for="propHideBorder" class="text-xs font-semibold text-gray-300">${trHtml('Hide Border')}</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="propTransparentBackground" ${field.transparentBackground ? 'checked' : ''} class="mr-2">
|
||||
<label for="propTransparentBackground" class="text-xs font-semibold text-gray-300">Transparent Background</label>
|
||||
<label for="propTransparentBackground" class="text-xs font-semibold text-gray-300">${trHtml('Transparent Background')}</label>
|
||||
</div>
|
||||
<button id="deleteBtn" class="w-full bg-red-600 text-white py-2 rounded hover:bg-red-700 transition text-sm font-semibold">
|
||||
Delete Field
|
||||
${trHtml('Delete Field')}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -1404,7 +1419,7 @@ function showProperties(field: FormField): void {
|
||||
|
||||
const validateName = (newName: string): boolean => {
|
||||
if (!newName) {
|
||||
nameError.textContent = 'Field name cannot be empty';
|
||||
nameError.textContent = trText('Field name cannot be empty');
|
||||
nameError.classList.remove('hidden');
|
||||
propName.classList.add('border-red-500');
|
||||
return false;
|
||||
@@ -1422,7 +1437,10 @@ function showProperties(field: FormField): void {
|
||||
const isDuplicateInPdf = existingFieldNames.has(newName);
|
||||
|
||||
if (isDuplicateInFields || isDuplicateInPdf) {
|
||||
nameError.textContent = `Field name "${newName}" already exists in this ${isDuplicateInPdf ? 'PDF' : 'form'}. Please try using a unique name.`;
|
||||
nameError.textContent = trText(
|
||||
'Field name "{{newName}}" already exists in this {{value}}. Please try using a unique name.',
|
||||
{ newName, value: isDuplicateInPdf ? trText('PDF') : trText('form') }
|
||||
);
|
||||
nameError.classList.remove('hidden');
|
||||
propName.classList.add('border-red-500');
|
||||
return false;
|
||||
@@ -1773,7 +1791,7 @@ function showProperties(field: FormField): void {
|
||||
if (propSelectedOption) {
|
||||
const currentVal = field.defaultValue;
|
||||
propSelectedOption.innerHTML =
|
||||
'<option value="">None</option>' +
|
||||
`<option value="">${trHtml('None')}</option>` +
|
||||
field.options
|
||||
?.map(
|
||||
(opt) =>
|
||||
@@ -1812,7 +1830,7 @@ function showProperties(field: FormField): void {
|
||||
const contentEl = fieldWrapper.querySelector(
|
||||
'.field-content'
|
||||
) as HTMLElement;
|
||||
if (contentEl) contentEl.textContent = field.label || 'Button';
|
||||
if (contentEl) contentEl.textContent = field.label || trText('Button');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2033,13 +2051,13 @@ function showProperties(field: FormField): void {
|
||||
};
|
||||
|
||||
const barcodeFormatHints: Record<string, string> = {
|
||||
qrcode: 'Any text, URL, or data',
|
||||
code128: 'ASCII characters (letters, numbers, symbols)',
|
||||
code39: 'Uppercase A-Z, digits 0-9, and - . $ / + % SPACE',
|
||||
ean13: 'Exactly 12 or 13 digits',
|
||||
upca: 'Exactly 11 or 12 digits',
|
||||
datamatrix: 'Any text, URL, or data',
|
||||
pdf417: 'Any text, URL, or data',
|
||||
qrcode: trText('Any text, URL, or data'),
|
||||
code128: trText('ASCII characters (letters, numbers, symbols)'),
|
||||
code39: trText('Uppercase A-Z, digits 0-9, and - . $ / + % SPACE'),
|
||||
ean13: trText('Exactly 12 or 13 digits'),
|
||||
upca: trText('Exactly 11 or 12 digits'),
|
||||
datamatrix: trText('Any text, URL, or data'),
|
||||
pdf417: trText('Any text, URL, or data'),
|
||||
};
|
||||
|
||||
const hintEl = document.getElementById('barcodeFormatHint');
|
||||
@@ -2069,8 +2087,7 @@ function showProperties(field: FormField): void {
|
||||
|
||||
// Hide properties panel
|
||||
function hideProperties(): void {
|
||||
propertiesPanel.innerHTML =
|
||||
'<p class="text-gray-500 text-sm">Select a field to edit properties</p>';
|
||||
propertiesPanel.innerHTML = `<p class="text-gray-500 text-sm">${trHtml('Select a field to edit properties')}</p>`;
|
||||
}
|
||||
|
||||
// Delete field
|
||||
@@ -2137,8 +2154,11 @@ downloadBtn.addEventListener('click', async () => {
|
||||
.map((name) => `"${name}"`)
|
||||
.join(', ');
|
||||
showModal(
|
||||
'Field Name Conflict',
|
||||
`The following field names already exist in the uploaded PDF: ${conflictList}. Please rename these fields before downloading.`,
|
||||
trText('Field Name Conflict'),
|
||||
trText(
|
||||
'The following field names already exist in the uploaded PDF: {{conflictList}}. Please rename these fields before downloading.',
|
||||
{ conflictList }
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
@@ -2147,20 +2167,23 @@ downloadBtn.addEventListener('click', async () => {
|
||||
if (duplicates.length > 0) {
|
||||
const duplicateList = duplicates.map((name) => `"${name}"`).join(', ');
|
||||
showModal(
|
||||
'Duplicate Field Names',
|
||||
`The following field names are used more than once: ${duplicateList}. Please rename these fields to use unique names before downloading.`,
|
||||
trText('Duplicate Field Names'),
|
||||
trText(
|
||||
'The following field names are used more than once: {{duplicateList}}. Please rename these fields to use unique names before downloading.',
|
||||
{ duplicateList }
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
alert('Please add at least one field before downloading.');
|
||||
alert(trText('Please add at least one field before downloading.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
alert('No pages found. Please create a blank PDF or upload one.');
|
||||
alert(trText('No pages found. Please create a blank PDF or upload one.'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2705,7 +2728,7 @@ downloadBtn.addEventListener('click', async () => {
|
||||
downloadFile(blob, uploadedFileName || 'document.pdf');
|
||||
showModal(
|
||||
'Success',
|
||||
'Your PDF has been downloaded successfully.',
|
||||
trText('Your PDF has been downloaded successfully.'),
|
||||
'info',
|
||||
() => {
|
||||
resetToInitial();
|
||||
@@ -2730,13 +2753,20 @@ downloadBtn.addEventListener('click', async () => {
|
||||
console.log(`Adding to existing radio group: ${fieldName}`);
|
||||
} else {
|
||||
showModal(
|
||||
'Duplicate Field Name',
|
||||
`A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`,
|
||||
trText('Duplicate Field Name'),
|
||||
trText(
|
||||
'A field named "{{fieldName}}" already exists. Please rename this field to use a unique name before downloading.',
|
||||
{ fieldName }
|
||||
),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showModal('Error', 'Error generating PDF: ' + errorMessage, 'error');
|
||||
showModal(
|
||||
'Error',
|
||||
trText('Error generating PDF: {{error}}', { error: errorMessage }),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2795,8 +2825,7 @@ function resetToInitial(): void {
|
||||
|
||||
canvas.innerHTML = '';
|
||||
|
||||
propertiesPanel.innerHTML =
|
||||
'<p class="text-gray-500 text-sm">Select a field to edit properties</p>';
|
||||
propertiesPanel.innerHTML = `<p class="text-gray-500 text-sm">${trHtml('Select a field to edit properties')}</p>`;
|
||||
|
||||
updateFieldCount();
|
||||
|
||||
@@ -2906,7 +2935,10 @@ async function renderCanvas(): Promise<void> {
|
||||
}
|
||||
|
||||
function updatePageNavigation(): void {
|
||||
pageIndicator.textContent = `Page ${currentPageIndex + 1} of ${pages.length}`;
|
||||
pageIndicator.textContent = trText('Page {{n}} of {{length}}', {
|
||||
n: currentPageIndex + 1,
|
||||
length: pages.length,
|
||||
});
|
||||
prevPageBtn.disabled = currentPageIndex === 0;
|
||||
nextPageBtn.disabled = currentPageIndex === pages.length - 1;
|
||||
}
|
||||
@@ -3067,7 +3099,7 @@ async function handlePdfUpload(file: File) {
|
||||
console.error('Error loading PDF:', error);
|
||||
showModal(
|
||||
'Error',
|
||||
'Error loading PDF file. Please try again with a valid PDF.',
|
||||
trText('Error loading PDF file. Please try again with a valid PDF.'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
@@ -3094,7 +3126,9 @@ addPageBtn.addEventListener('click', () => {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (fields.length > 0 || pages.length > 0) {
|
||||
if (
|
||||
confirm('Are you sure you want to reset? All your work will be lost.')
|
||||
confirm(
|
||||
trText('Are you sure you want to reset? All your work will be lost.')
|
||||
)
|
||||
) {
|
||||
resetToInitial();
|
||||
}
|
||||
@@ -3121,9 +3155,9 @@ function showModal(
|
||||
if (!errorModal || !errorModalTitle || !errorModalMessage || !errorModalClose)
|
||||
return;
|
||||
|
||||
errorModalTitle.textContent = title;
|
||||
errorModalMessage.textContent = message;
|
||||
errorModalClose.textContent = buttonText;
|
||||
errorModalTitle.textContent = trText(title);
|
||||
errorModalMessage.textContent = trText(message);
|
||||
errorModalClose.textContent = trText(buttonText);
|
||||
|
||||
modalCloseCallback = onClose || null;
|
||||
errorModal.classList.remove('hidden');
|
||||
|
||||
@@ -3,6 +3,9 @@ import { createIcons, icons } from 'lucide';
|
||||
import { getPDFDocument } from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
import { getStoredItem } from '../utils/safe-storage.js';
|
||||
|
||||
let viewerIframe: HTMLIFrameElement | null = null;
|
||||
let viewerReady = false;
|
||||
let currentFile: File | null = null;
|
||||
@@ -12,7 +15,7 @@ function showLoader(message: string = 'Processing...') {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loader) loader.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = message;
|
||||
if (loaderText) loaderText.textContent = trText(message);
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
@@ -31,8 +34,8 @@ function showAlert(
|
||||
const alertMessage = document.getElementById('alert-message');
|
||||
const okBtn = document.getElementById('alert-ok');
|
||||
|
||||
if (alertTitle) alertTitle.textContent = title;
|
||||
if (alertMessage) alertMessage.textContent = message;
|
||||
if (alertTitle) alertTitle.textContent = trText(title);
|
||||
if (alertMessage) alertMessage.textContent = trText(message);
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
|
||||
if (okBtn) {
|
||||
@@ -80,7 +83,7 @@ function updateFileDisplay() {
|
||||
removeBtn.id = 'remove-file';
|
||||
removeBtn.className =
|
||||
'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2';
|
||||
removeBtn.title = 'Remove file';
|
||||
removeBtn.title = trText('Remove file');
|
||||
|
||||
const removeIcon = document.createElement('i');
|
||||
removeIcon.setAttribute('data-lucide', 'trash-2');
|
||||
@@ -117,7 +120,7 @@ function resetState() {
|
||||
}
|
||||
|
||||
const toolUploader = document.getElementById('tool-uploader');
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||
const isFullWidth = getStoredItem('fullWidthMode') !== 'false';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-6xl');
|
||||
toolUploader.classList.add('max-w-2xl');
|
||||
@@ -181,7 +184,7 @@ async function setupFormViewer() {
|
||||
|
||||
const toolUploader = document.getElementById('tool-uploader');
|
||||
// Default to true if not set
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
|
||||
const isFullWidth = getStoredItem('fullWidthMode') !== 'false';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-2xl');
|
||||
toolUploader.classList.add('max-w-6xl');
|
||||
@@ -281,7 +284,7 @@ async function processAndDownloadForm() {
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -11,13 +11,11 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { HeaderFooterState } from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: HeaderFooterState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -100,7 +98,10 @@ function updateFileDisplay() {
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{getPageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
getPageCount: pageState.pdfDoc.getPageCount(),
|
||||
});
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
@@ -167,7 +168,7 @@ async function addHeaderFooter() {
|
||||
};
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0)
|
||||
throw new Error('Invalid page range specified.');
|
||||
throw new Error(trText('Invalid page range specified.'));
|
||||
const drawOptions = {
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
|
||||
+154
-143
@@ -4,176 +4,187 @@ import { createIcons, icons } from 'lucide';
|
||||
import heic2any from 'heic2any';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: 'image/png',
|
||||
quality: 0.92,
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult)
|
||||
? conversionResult[0]
|
||||
: conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_heic.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert HEIC to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: 'image/png',
|
||||
quality: 0.92,
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult)
|
||||
? conversionResult[0]
|
||||
: conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_heic.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert HEIC to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) =>
|
||||
file.name.toLowerCase().endsWith('.heic') ||
|
||||
file.name.toLowerCase().endsWith('.heif')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert(
|
||||
'Invalid Files',
|
||||
'Some files were skipped. Only HEIC/HEIF files are allowed.'
|
||||
);
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only HEIC/HEIF files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
};
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -14,14 +14,12 @@ import {
|
||||
preprocessImageFile,
|
||||
} from '@/js/utils/image-input-utils.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDFInstance | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -143,7 +141,9 @@ function updateUI() {
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: InvertColorsState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -93,7 +91,10 @@ function updateFileDisplay() {
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{getPageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
getPageCount: pageState.pdfDoc.getPageCount(),
|
||||
});
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
@@ -126,7 +127,12 @@ async function invertColors() {
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdfjsDoc.numPages}...`);
|
||||
showLoader(
|
||||
trText('Processing page {{i}} of {{numPages}}...', {
|
||||
i,
|
||||
numPages: pdfjsDoc.numPages,
|
||||
})
|
||||
);
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
@@ -8,17 +8,15 @@ import {
|
||||
compressImageFile,
|
||||
} from '../utils/image-compress.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx';
|
||||
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDFInstance | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -146,7 +144,9 @@ function updateUI() {
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
import { LinearizePdfState, QpdfInstanceExtended } from '@/types';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: LinearizePdfState = {
|
||||
files: [],
|
||||
};
|
||||
@@ -107,7 +109,9 @@ async function linearizePdf() {
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText)
|
||||
loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
|
||||
loaderText.textContent = trText(
|
||||
'Optimizing PDFs for web view (linearizing)...'
|
||||
);
|
||||
|
||||
const zip = new JSZip();
|
||||
const usedNames = new Set<string>();
|
||||
@@ -124,7 +128,10 @@ async function linearizePdf() {
|
||||
const outputPath = `/output_${i}.pdf`;
|
||||
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
|
||||
loaderText.textContent = trText(
|
||||
'Optimizing {{name}} ({{n}}/{{length}})...',
|
||||
{ name: file.name, n: i + 1, length: pdfFiles.length }
|
||||
);
|
||||
|
||||
try {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
@@ -141,7 +148,9 @@ async function linearizePdf() {
|
||||
console.error(
|
||||
`Linearization resulted in an empty file for ${file.name}.`
|
||||
);
|
||||
throw new Error(`Processing failed for ${file.name}.`);
|
||||
throw new Error(
|
||||
trText('Processing failed for {{name}}.', { name: file.name })
|
||||
);
|
||||
}
|
||||
|
||||
const zipEntryName = deduplicateFileName(
|
||||
@@ -173,16 +182,19 @@ async function linearizePdf() {
|
||||
}
|
||||
|
||||
if (successCount === 0) {
|
||||
throw new Error('No PDF files could be linearized.');
|
||||
throw new Error(trText('No PDF files could be linearized.'));
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
|
||||
if (loaderText) loaderText.textContent = trText('Generating ZIP file...');
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'linearized-pdfs.zip');
|
||||
|
||||
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
|
||||
let alertMessage = trText(
|
||||
'{{successCount}} PDF(s) linearized successfully.',
|
||||
{ successCount }
|
||||
);
|
||||
if (errorCount > 0) {
|
||||
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||
alertMessage += ` ${trText('Files that failed: {{errorCount}}.', { errorCount })}`;
|
||||
}
|
||||
showAlert('Processing Complete', alertMessage, 'success', () => {
|
||||
resetState();
|
||||
@@ -191,14 +203,16 @@ async function linearizePdf() {
|
||||
console.error('Linearization process error:', error);
|
||||
showAlert(
|
||||
'Linearization Failed',
|
||||
`An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}.`
|
||||
trText('An error occurred: {{error}}.', {
|
||||
error: error instanceof Error ? error.message : trText('Unknown error'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { MarkdownEditor } from '../utils/markdown-editor.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
import { onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
onPageReady(() => {
|
||||
const container = document.getElementById('markdown-editor-container');
|
||||
|
||||
if (!container) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
showWasmRequiredDialog,
|
||||
WasmProvider,
|
||||
} from '../utils/wasm-provider.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
@@ -166,11 +166,14 @@ async function renderPageMergeThumbnails() {
|
||||
'text-xs text-gray-400 truncate w-full text-center';
|
||||
const fullTitle = displayName
|
||||
? `${displayName} (page ${pageNumber})`
|
||||
: `Page ${pageNumber}`;
|
||||
: trText('Page {{pageNumber}}', { pageNumber });
|
||||
fileNamePara.title = fullTitle;
|
||||
fileNamePara.textContent = displayName
|
||||
? `${displayName.substring(0, 10)}... (p${pageNumber})`
|
||||
: `Page ${pageNumber}`;
|
||||
? trText('{{name}}... (p{{pageNumber}})', {
|
||||
name: displayName.substring(0, 10),
|
||||
pageNumber,
|
||||
})
|
||||
: trText('Page {{pageNumber}}', { pageNumber });
|
||||
|
||||
wrapper.append(imgContainer, fileNamePara);
|
||||
return wrapper;
|
||||
@@ -291,7 +294,7 @@ export async function merge() {
|
||||
|
||||
if (mergeState.activeMode === 'file') {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
if (!fileList) throw new Error(trText('File list not found'));
|
||||
|
||||
const sortedFileKeys = Array.from(fileList.children)
|
||||
.map((li) => (li as HTMLElement).dataset.fileName)
|
||||
@@ -321,7 +324,7 @@ export async function merge() {
|
||||
} else {
|
||||
// Page Mode
|
||||
const pageContainer = document.getElementById('page-merge-preview');
|
||||
if (!pageContainer) throw new Error('Page container not found');
|
||||
if (!pageContainer) throw new Error(trText('Page container not found'));
|
||||
const pageElements = Array.from(pageContainer.children);
|
||||
|
||||
const rawPages: { fileName: string; pageIndex: number }[] = [];
|
||||
@@ -523,14 +526,16 @@ export async function refreshMergeUI() {
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `range-${safeFileName}`;
|
||||
label.className = 'text-xs text-gray-400';
|
||||
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
||||
label.textContent = trText('Pages (e.g., 1-3, 5) - Total: {{pageCount}}', {
|
||||
pageCount,
|
||||
});
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `range-${safeFileName}`;
|
||||
input.className =
|
||||
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||
input.placeholder = 'Leave blank for all pages';
|
||||
input.placeholder = trText('Leave blank for all pages');
|
||||
|
||||
inputWrapper.append(label, input);
|
||||
|
||||
@@ -538,7 +543,7 @@ export async function refreshMergeUI() {
|
||||
deleteBtn.className =
|
||||
'text-red-400 hover:text-red-300 p-2 flex-shrink-0 self-end';
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
deleteBtn.title = 'Remove file';
|
||||
deleteBtn.title = trText('Remove file');
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
@@ -603,7 +608,7 @@ export async function refreshMergeUI() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -4,11 +4,13 @@ import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const FILETYPE = 'mobi';
|
||||
const EXTENSIONS = ['.mobi'];
|
||||
const TOOL_NAME = 'MOBI';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -83,7 +85,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const convertToPdf = async () => {
|
||||
try {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{TOOL_NAME}} file.', {
|
||||
TOOL_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -92,7 +99,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const originalFile = state.files[0];
|
||||
showLoader(`Converting ${originalFile.name}...`);
|
||||
showLoader(
|
||||
trText('Converting {{name}}...', { name: originalFile.name })
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||
filetype: FILETYPE,
|
||||
@@ -104,7 +113,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -116,7 +127,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||
@@ -134,7 +149,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||
trText(
|
||||
'Successfully converted {{length}} {{TOOL_NAME}} file(s) to PDF.',
|
||||
{ length: state.files.length, TOOL_NAME }
|
||||
),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -144,7 +162,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface NUpState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
@@ -51,7 +53,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -79,7 +83,10 @@ async function updateUI() {
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
@@ -152,7 +159,9 @@ async function nUpTool() {
|
||||
const usableHeight = pageHeight - margin * 2;
|
||||
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
showLoader(
|
||||
trText('Processing sheet {{n}}...', { n: Math.floor(i / n) + 1 })
|
||||
);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
@@ -234,7 +243,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
resolveConfiguredTesseractAvailableLanguages,
|
||||
UnsupportedOcrLanguageError,
|
||||
} from '../utils/tesseract-language-availability.js';
|
||||
import { initI18n, t } from '../i18n/index.js';
|
||||
import { initI18n, t, onPageReady } from '../i18n/index.js';
|
||||
|
||||
const pageState: OcrState = {
|
||||
file: null,
|
||||
@@ -285,7 +285,7 @@ function populateLanguageList() {
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
onPageReady(async function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById(
|
||||
|
||||
+215
-179
@@ -1,189 +1,225 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
import {
|
||||
getLibreOfficeConverter,
|
||||
type LoadProgress,
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.odg'];
|
||||
const FILETYPE_NAME = 'ODG';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className =
|
||||
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{FILETYPE_NAME}} file.', {
|
||||
FILETYPE_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{length}} files to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message =
|
||||
err instanceof Error ? err.message : trText('Unknown error');
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('An error occurred during conversion. Error: {{message}}', {
|
||||
message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
+215
-179
@@ -1,189 +1,225 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
import {
|
||||
getLibreOfficeConverter,
|
||||
type LoadProgress,
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.odp'];
|
||||
const FILETYPE_NAME = 'ODP';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className =
|
||||
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{FILETYPE_NAME}} file.', {
|
||||
FILETYPE_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{length}} files to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message =
|
||||
err instanceof Error ? err.message : trText('Unknown error');
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('An error occurred during conversion. Error: {{message}}', {
|
||||
message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
+215
-179
@@ -1,189 +1,225 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
formatBytes,
|
||||
} from '../utils/helpers.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
|
||||
import {
|
||||
getLibreOfficeConverter,
|
||||
type LoadProgress,
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.ods'];
|
||||
const FILETYPE_NAME = 'ODS';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className =
|
||||
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className =
|
||||
'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const convertOptions = document.getElementById('convert-options');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = async () => {
|
||||
if (!convertOptions) return;
|
||||
|
||||
if (state.files.length > 0) {
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
for (let index = 0; index < state.files.length; index++) {
|
||||
const file = state.files[index];
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
if (fileControls) fileControls.classList.remove('hidden');
|
||||
convertOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
convertOptions.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
state.files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
};
|
||||
|
||||
const convert = async () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert(
|
||||
'No Files',
|
||||
trText('Please select at least one {{FILETYPE_NAME}} file.', {
|
||||
FILETYPE_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = getLibreOfficeConverter();
|
||||
|
||||
showLoader('Loading engine...');
|
||||
await converter.initialize((progress: LoadProgress) => {
|
||||
showLoader(progress.message, progress.percent);
|
||||
});
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
} else {
|
||||
showLoader('Converting multiple files...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
zip.file(`${baseName}.pdf`, pdfBlob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
trText('Successfully converted {{length}} files to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
hideLoader();
|
||||
const message =
|
||||
err instanceof Error ? err.message : trText('Unknown error');
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('An error occurred during conversion. Error: {{message}}', {
|
||||
message,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const validFiles = Array.from(files).filter((file) => {
|
||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||
});
|
||||
if (validFiles.length > 0) {
|
||||
state.files = [...state.files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
});
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
type LoadProgress,
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -113,7 +115,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: originalFile.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -125,7 +129,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
@@ -143,7 +151,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} ODT file(s) to PDF.`,
|
||||
trText('Successfully converted {{length}} ODT file(s) to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -152,7 +162,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ import Sortable from 'sortablejs';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface OrganizeState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFDocument | null;
|
||||
@@ -25,11 +27,7 @@ const organizeState: OrganizeState = {
|
||||
sortableInstance: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -97,7 +95,10 @@ function applyCustomOrder() {
|
||||
if (newOrder.length !== currentGridCount) {
|
||||
showAlert(
|
||||
'Incorrect Page Count',
|
||||
`The number of pages specified (${newOrder.length}) does not match the current number of pages in the document (${currentGridCount}). Please provide a complete ordering for all pages.`
|
||||
trText(
|
||||
'The number of pages specified ({{length}}) does not match the current number of pages in the document ({{currentGridCount}}). Please provide a complete ordering for all pages.',
|
||||
{ length: newOrder.length, currentGridCount }
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -206,7 +207,10 @@ function updateFileDisplay() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{totalPages}} pages', {
|
||||
size: formatBytes(organizeState.file.size),
|
||||
totalPages: organizeState.totalPages,
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -306,13 +310,13 @@ async function renderThumbnails() {
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className =
|
||||
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
duplicateBtn.title = 'Duplicate Page';
|
||||
duplicateBtn.title = trText('Duplicate Page');
|
||||
duplicateBtn.innerHTML = '<i data-lucide="copy-plus" class="w-5 h-5"></i>';
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className =
|
||||
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
deleteBtn.title = 'Delete Page';
|
||||
deleteBtn.title = trText('Delete Page');
|
||||
deleteBtn.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5"></i>';
|
||||
|
||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import type { OverlayPdfState, QpdfInstanceExtended } from '@/types';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: OverlayPdfState = {
|
||||
baseFile: null,
|
||||
overlayFile: null,
|
||||
@@ -119,7 +121,7 @@ async function processOverlay() {
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing PDF engine...';
|
||||
if (loaderText) loaderText.textContent = trText('Initializing PDF engine...');
|
||||
|
||||
const inputPath = '/input_base.pdf';
|
||||
const overlayPath = '/input_overlay.pdf';
|
||||
@@ -129,7 +131,7 @@ async function processOverlay() {
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading files...';
|
||||
if (loaderText) loaderText.textContent = trText('Reading files...');
|
||||
|
||||
const baseBuffer = await readFileAsArrayBuffer(pageState.baseFile);
|
||||
const overlayBuffer = await readFileAsArrayBuffer(pageState.overlayFile);
|
||||
@@ -155,7 +157,9 @@ async function processOverlay() {
|
||||
const shouldRepeat = repeatCheckbox?.checked;
|
||||
|
||||
if (loaderText)
|
||||
loaderText.textContent = `Applying ${mode.replace('--', '')}...`;
|
||||
loaderText.textContent = trText('Applying {{replace}}...', {
|
||||
replace: mode.replace('--', ''),
|
||||
});
|
||||
|
||||
const args = [inputPath, mode, overlayPath];
|
||||
|
||||
@@ -173,7 +177,7 @@ async function processOverlay() {
|
||||
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing produced an empty file.');
|
||||
throw new Error(trText('Processing produced an empty file.'));
|
||||
}
|
||||
|
||||
const modeLabel = mode.replace('--', '');
|
||||
@@ -184,7 +188,7 @@ async function processOverlay() {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`PDF ${modeLabel} applied successfully.`,
|
||||
trText('PDF {{modeLabel}} applied successfully.', { modeLabel }),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
@@ -194,7 +198,9 @@ async function processOverlay() {
|
||||
console.error('Overlay/underlay error:', error);
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}.`
|
||||
trText('An error occurred: {{error}}.', {
|
||||
error: error instanceof Error ? error.message : trText('Unknown error'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
@@ -247,7 +253,7 @@ function setupDropZone(
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const baseDropZone = document.getElementById('base-drop-zone');
|
||||
const baseInput = document.getElementById(
|
||||
'base-file-input'
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from '@/types';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, trHtml, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const pageState: PageDimensionsState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
@@ -88,17 +90,17 @@ function renderSummary() {
|
||||
let summaryHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
|
||||
<p class="text-sm text-gray-400 mb-1">${trHtml('Total Pages')}</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
|
||||
<p class="text-sm text-gray-400 mb-1">${trHtml('Unique Page Sizes')}</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Document Type</p>
|
||||
<p class="text-sm text-gray-400 mb-1">${trHtml('Document Type')}</p>
|
||||
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
|
||||
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
|
||||
${stats.hasMixedSizes ? trText('Mixed Sizes') : trText('Uniform')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,13 +112,13 @@ function renderSummary() {
|
||||
<div class="flex items-start gap-3">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
|
||||
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
|
||||
<h4 class="text-yellow-200 font-semibold mb-2">${trHtml('Mixed Page Sizes Detected')}</h4>
|
||||
<p class="text-sm text-gray-300 mb-3">${trHtml('This document contains pages with different dimensions:')}</p>
|
||||
<ul class="space-y-1 text-sm text-gray-300">
|
||||
${stats.uniqueSizes
|
||||
.map(
|
||||
(size: UniqueSizeEntry) => `
|
||||
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
||||
<li>${size.count > 1 ? trHtml('• {{label}}: {{count}} pages', { label: size.label, count: size.count }) : trHtml('• {{label}}: 1 page', { label: size.label })}</li>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
@@ -154,7 +156,11 @@ function renderTable(unit: string) {
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
dimensionsCell.textContent = trText('{{width}} x {{height}} {{unit}}', {
|
||||
width,
|
||||
height,
|
||||
unit,
|
||||
});
|
||||
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
@@ -174,7 +180,9 @@ function renderTable(unit: string) {
|
||||
|
||||
const rotationCell = document.createElement('td');
|
||||
rotationCell.className = 'px-4 py-3 text-gray-300';
|
||||
rotationCell.textContent = `${pageData.rotation}°`;
|
||||
rotationCell.textContent = trText('{{rotation}}°', {
|
||||
rotation: pageData.rotation,
|
||||
});
|
||||
|
||||
row.append(
|
||||
pageNumCell,
|
||||
@@ -196,16 +204,20 @@ function exportToCSV() {
|
||||
const unit = unitsSelect?.value || 'pt';
|
||||
|
||||
const headers = [
|
||||
'Page #',
|
||||
`Width (${unit})`,
|
||||
`Height (${unit})`,
|
||||
'Standard Size',
|
||||
'Orientation',
|
||||
'Aspect Ratio',
|
||||
`Area (${unit}²)`,
|
||||
'Rotation',
|
||||
trText('Page #'),
|
||||
trText('Width ({{unit}})', { unit }),
|
||||
trText('Height ({{unit}})', { unit }),
|
||||
trText('Standard Size'),
|
||||
trText('Orientation'),
|
||||
trText('Aspect Ratio'),
|
||||
trText('Area ({{unit}}²)', { unit }),
|
||||
trText('Rotation'),
|
||||
];
|
||||
const csvRows = [headers.join(',')];
|
||||
const csvCell = (value: string | number) => {
|
||||
const text = String(value);
|
||||
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
||||
};
|
||||
const csvRows = [headers.map(csvCell).join(',')];
|
||||
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
@@ -223,7 +235,7 @@ function exportToCSV() {
|
||||
area,
|
||||
`${pageData.rotation}°`,
|
||||
];
|
||||
csvRows.push(row.join(','));
|
||||
csvRows.push(row.map(csvCell).join(','));
|
||||
});
|
||||
|
||||
const csvContent = csvRows.join('\n');
|
||||
@@ -252,8 +264,11 @@ function analyzeAndDisplayDimensions() {
|
||||
pageNum: index + 1,
|
||||
width,
|
||||
height,
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
orientation: width > height ? trText('Landscape') : trText('Portrait'),
|
||||
standardSize: (() => {
|
||||
const name = getStandardPageName(width, height);
|
||||
return name === 'Custom' ? trText('Custom') : name;
|
||||
})(),
|
||||
rotation: rotation,
|
||||
});
|
||||
});
|
||||
@@ -357,7 +372,7 @@ async function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from '../utils/pdf-operations.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
@@ -20,11 +22,7 @@ const pageState: PageState = {
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -122,7 +120,10 @@ function updateFileDisplay() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{getPageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
getPageCount: pageState.pdfDoc.getPageCount(),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
} from '../utils/libreoffice-loader.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ['.pages'];
|
||||
const FILETYPE_NAME = 'Pages';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
state.files = [];
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
@@ -90,7 +92,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (state.files.length === 0) {
|
||||
showAlert(
|
||||
'No Files',
|
||||
`Please select at least one ${FILETYPE_NAME} file.`
|
||||
trText('Please select at least one {{FILETYPE_NAME}} file.', {
|
||||
FILETYPE_NAME,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -105,7 +109,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
@@ -114,7 +118,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${file.name} to PDF.`,
|
||||
trText('Successfully converted {{name}} to PDF.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -127,7 +133,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
const pdfBlob = await converter.convertToPdf(file);
|
||||
|
||||
@@ -145,7 +155,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} files to PDF.`,
|
||||
trText('Successfully converted {{length}} files to PDF.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -155,7 +167,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
import { trText, trHtml, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface BookletState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
@@ -38,8 +40,7 @@ function resetState() {
|
||||
|
||||
const previewArea = document.getElementById('booklet-preview');
|
||||
if (previewArea)
|
||||
previewArea.innerHTML =
|
||||
'<p class="text-gray-400 text-center py-8">Upload a PDF and click "Generate Preview" to see the booklet layout</p>';
|
||||
previewArea.innerHTML = `<p class="text-gray-400 text-center py-8">${trHtml('Upload a PDF and click "Generate Preview" to see the booklet layout')}</p>`;
|
||||
|
||||
const downloadBtn = document.getElementById(
|
||||
'download-btn'
|
||||
@@ -69,7 +70,9 @@ async function updateUI() {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
metaSpan.textContent = trText('{{size}} • Loading...', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -100,7 +103,10 @@ async function updateUI() {
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{pageCount}} pages', {
|
||||
size: formatBytes(pageState.file.size),
|
||||
pageCount,
|
||||
});
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
|
||||
@@ -197,8 +203,7 @@ async function generatePreview() {
|
||||
const canvasWidth = containerWidth;
|
||||
const canvasHeight = containerWidth / aspectRatio;
|
||||
|
||||
previewArea.innerHTML =
|
||||
'<p class="text-gray-400 text-center py-4">Generating preview...</p>';
|
||||
previewArea.innerHTML = `<p class="text-gray-400 text-center py-4">${trHtml('Generating preview...')}</p>`;
|
||||
|
||||
const totalRounded = isBookletMode
|
||||
? Math.ceil(totalPages / 4) * 4
|
||||
@@ -234,7 +239,7 @@ async function generatePreview() {
|
||||
}
|
||||
}
|
||||
|
||||
previewArea.innerHTML = `<p class="text-indigo-400 text-sm mb-4 text-center">${totalPages} pages → ${numSheets} output sheets</p>`;
|
||||
previewArea.innerHTML = `<p class="text-indigo-400 text-sm mb-4 text-center">${trHtml('{{totalPages}} pages → {{numSheets}} output sheets', { totalPages, numSheets })}</p>`;
|
||||
|
||||
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -375,9 +380,16 @@ async function generatePreview() {
|
||||
ctx.font = 'bold 10px sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.textBaseline = 'top';
|
||||
const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : '';
|
||||
const sideLabel = isBookletMode
|
||||
? isFront
|
||||
? trText('Front')
|
||||
: trText('Back')
|
||||
: '';
|
||||
ctx.fillText(
|
||||
`Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`,
|
||||
trText('Sheet {{n}} {{sideLabel}}', {
|
||||
n: Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1,
|
||||
sideLabel,
|
||||
}),
|
||||
canvasWidth - 6,
|
||||
4
|
||||
);
|
||||
@@ -527,7 +539,7 @@ async function createBooklet() {
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`Booklet created with ${numSheets} sheets!`,
|
||||
trText('Booklet created with {{numSheets}} sheets!', { numSheets }),
|
||||
'success',
|
||||
function () {
|
||||
resetState();
|
||||
@@ -554,7 +566,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const previewBtn = document.getElementById('preview-btn');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, trHtml, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
downloadFile,
|
||||
escapeHtml,
|
||||
@@ -41,7 +41,7 @@ let currentDoc: PyMuPDFDocument | null = null;
|
||||
const layersMap = new Map<number, LayerData>();
|
||||
let nextDisplayOrder = 0;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -75,7 +75,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(currentFile.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -93,10 +96,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • ${pdfDoc.numPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} pages', {
|
||||
size: formatBytes(currentFile.size),
|
||||
numPages: pdfDoc.numPages,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
|
||||
metaSpan.textContent = trText('{{size}} • Could not load page count', {
|
||||
size: formatBytes(currentFile.size),
|
||||
});
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
@@ -191,8 +199,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (layersArray.length === 0) {
|
||||
layersList.innerHTML = `
|
||||
<div class="layers-empty">
|
||||
<p>This PDF has no layers (OCG).</p>
|
||||
<p>Add a new layer to get started!</p>
|
||||
<p>${trHtml('This PDF has no layers (OCG).')}</p>
|
||||
<p>${trHtml('Add a new layer to get started!')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -209,12 +217,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="layer-item" data-number="${layer.number}" style="padding-left: ${layer.depth * 24 + 8}px;">
|
||||
<label class="layer-toggle">
|
||||
<input type="checkbox" ${layer.on ? 'checked' : ''} ${layer.locked ? 'disabled' : ''} data-xref="${layer.xref}" />
|
||||
<span class="layer-name">${layer.depth > 0 ? '└ ' : ''}${escapeHtml(layer.text || `Layer ${layer.number}`)}</span>
|
||||
<span class="layer-name">${layer.depth > 0 ? '└ ' : ''}${escapeHtml(layer.text || trText('Layer {{number}}', { number: layer.number }))}</span>
|
||||
${layer.locked ? '<span class="layer-locked">🔒</span>' : ''}
|
||||
</label>
|
||||
<div class="layer-actions">
|
||||
${!layer.locked ? `<button class="layer-add-child" data-xref="${layer.xref}" title="Add child layer">+</button>` : ''}
|
||||
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="Delete layer">✕</button>` : ''}
|
||||
${!layer.locked ? `<button class="layer-add-child" data-xref="${layer.xref}" title="${trHtml('Add child layer')}">+</button>` : ''}
|
||||
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="${trHtml('Delete layer')}">✕</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -280,8 +288,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
);
|
||||
|
||||
const childName = await promptForInput(
|
||||
'Add Child Layer',
|
||||
`Enter name for child layer under "${parentLayer?.text || 'Layer'}":`
|
||||
trText('Add Child Layer'),
|
||||
trText('Enter name for child layer under "{{Layer}}":', {
|
||||
Layer: parentLayer?.text || 'Layer',
|
||||
})
|
||||
);
|
||||
|
||||
if (!childName || !childName.trim()) return;
|
||||
@@ -328,7 +338,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
showLoader('Loading engine...');
|
||||
const pymupdf = await loadPyMuPDF();
|
||||
|
||||
showLoader(`Loading layers from ${currentFile.name}...`);
|
||||
showLoader(
|
||||
trText('Loading layers from {{name}}...', { name: currentFile.name })
|
||||
);
|
||||
currentDoc = await (
|
||||
pymupdf as { open: (file: File) => Promise<PyMuPDFDocument> }
|
||||
).open(currentFile);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { repairPdfFile } from './repair-pdf.js';
|
||||
import { partitionIncomingFiles } from '../utils/multi-tool-file-input.js';
|
||||
import { convertImagesToPdfFile } from '../utils/images-to-pdf-lib.js';
|
||||
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -85,8 +85,8 @@ function showModal(
|
||||
|
||||
if (!modal || !modalTitle || !modalMessage || !modalIcon) return;
|
||||
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.textContent = message;
|
||||
modalTitle.textContent = trText(title);
|
||||
modalMessage.textContent = trText(message);
|
||||
|
||||
const iconMap = {
|
||||
info: 'info',
|
||||
@@ -162,17 +162,7 @@ function hideLoading() {
|
||||
if (loader) loader.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('PDF Multi Tool: DOMContentLoaded');
|
||||
initializeTool();
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
'PDF Multi Tool: DOMContentLoaded already fired, initializing immediately'
|
||||
);
|
||||
initializeTool();
|
||||
}
|
||||
onPageReady(initializeTool);
|
||||
|
||||
function initializeTool() {
|
||||
console.log('PDF Multi Tool: Initializing...');
|
||||
@@ -514,7 +504,9 @@ async function loadPdfs(files: File[]) {
|
||||
console.log(`Repairing ${file.name}...`);
|
||||
const loadingText = document.getElementById('loading-text');
|
||||
if (loadingText)
|
||||
loadingText.textContent = `Repairing ${file.name}...`;
|
||||
loadingText.textContent = trText('Repairing {{name}}...', {
|
||||
name: file.name,
|
||||
});
|
||||
|
||||
const repairedData = await repairPdfFile(file);
|
||||
if (repairedData) {
|
||||
@@ -680,7 +672,10 @@ function createPageElement(
|
||||
// Page info
|
||||
const info = document.createElement('div');
|
||||
info.className = 'text-xs text-gray-400 text-center mb-2';
|
||||
info.textContent = `${t('common.page')} ${index + 1}`;
|
||||
info.textContent = trText('{{page}} {{n}}', {
|
||||
page: t('common.page'),
|
||||
n: index + 1,
|
||||
});
|
||||
|
||||
// Actions toolbar
|
||||
const actions = document.createElement('div');
|
||||
@@ -1026,7 +1021,7 @@ async function handleInsertPdf(e: Event) {
|
||||
console.error('Failed to insert PDF:', e);
|
||||
showModal(
|
||||
'Error',
|
||||
'Failed to insert PDF. The file may be corrupted.',
|
||||
trText('Failed to insert PDF. The file may be corrupted.'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
@@ -1085,7 +1080,11 @@ function addBlankPage() {
|
||||
|
||||
function bulkRotate(delta: number) {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Selection', 'Please select pages to rotate.', 'info');
|
||||
showModal(
|
||||
trText('No Selection'),
|
||||
trText('Please select pages to rotate.'),
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1117,7 +1116,11 @@ function bulkRotate(delta: number) {
|
||||
|
||||
function bulkDelete() {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Selection', 'Please select pages to delete.', 'info');
|
||||
showModal(
|
||||
trText('No Selection'),
|
||||
trText('Please select pages to delete.'),
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const indices = Array.from(selectedPages).sort((a, b) => b - a);
|
||||
@@ -1134,7 +1137,11 @@ function bulkDelete() {
|
||||
|
||||
function bulkDuplicate() {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Selection', 'Please select pages to duplicate.', 'info');
|
||||
showModal(
|
||||
trText('No Selection'),
|
||||
trText('Please select pages to duplicate.'),
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const indices = Array.from(selectedPages).sort((a, b) => b - a);
|
||||
@@ -1148,8 +1155,8 @@ function bulkDuplicate() {
|
||||
function bulkSplit() {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal(
|
||||
'No Selection',
|
||||
'Please select pages to mark for splitting.',
|
||||
trText('No Selection'),
|
||||
trText('Please select pages to mark for splitting.'),
|
||||
'info'
|
||||
);
|
||||
return;
|
||||
@@ -1167,7 +1174,7 @@ function bulkSplit() {
|
||||
|
||||
async function downloadAll() {
|
||||
if (allPages.length === 0) {
|
||||
showModal('No Pages', 'Please upload PDFs first.', 'info');
|
||||
showModal(trText('No Pages'), trText('Please upload PDFs first.'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1278,12 +1285,14 @@ async function downloadSplitPdfs() {
|
||||
|
||||
showModal(
|
||||
'Success',
|
||||
`Downloaded ${segments.length} PDF files in a ZIP archive.`,
|
||||
trText('Downloaded {{length}} PDF files in a ZIP archive.', {
|
||||
length: segments.length,
|
||||
}),
|
||||
'success'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to create split PDFs:', e);
|
||||
showModal('Error', 'Failed to create split PDFs.', 'error');
|
||||
showModal('Error', trText('Failed to create split PDFs.'), 'error');
|
||||
} finally {
|
||||
hideLoading(); // Ensure loader is hidden if we used it (though showModal replaces it)
|
||||
}
|
||||
@@ -1358,10 +1367,10 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
|
||||
});
|
||||
|
||||
downloadFile(blob, filename);
|
||||
showModal('Success', 'PDF downloaded successfully.', 'success');
|
||||
showModal('Success', trText('PDF downloaded successfully.'), 'success');
|
||||
} catch (e) {
|
||||
console.error('Failed to create PDF:', e);
|
||||
showModal('Error', 'Failed to create PDF.', 'error');
|
||||
showModal('Error', trText('Failed to create PDF.'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,7 +1409,7 @@ function updatePageDisplay() {
|
||||
const info = card.querySelector(
|
||||
'.text-xs.text-gray-400.text-center.mb-2'
|
||||
);
|
||||
if (info) info.textContent = `Page ${index + 1} `;
|
||||
if (info) info.textContent = trText('Page {{n}} ', { n: index + 1 });
|
||||
|
||||
// Update selection state
|
||||
const selectBtn = card.querySelector(
|
||||
@@ -1508,7 +1517,7 @@ function updatePageNumbers() {
|
||||
// Update visible page number text
|
||||
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
|
||||
if (info) {
|
||||
info.textContent = `Page ${index + 1} `;
|
||||
info.textContent = trText('Page {{n}} ', { n: index + 1 });
|
||||
}
|
||||
|
||||
// Re-attach event listeners for buttons
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -42,7 +42,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
}); // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -65,7 +68,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -154,7 +161,7 @@ async function renderPage(page: PDFPageProxy): Promise<Blob | null> {
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
generateComicInfoXml,
|
||||
generateMetadataOpf,
|
||||
@@ -112,7 +112,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -131,7 +134,11 @@ const updateUI = () => {
|
||||
readFileAsArrayBuffer(file)
|
||||
.then((buffer) => getPDFDocument(buffer).promise)
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
@@ -180,7 +187,7 @@ async function renderPage(
|
||||
const viewport = page.getViewport({ scale: options.scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) throw new Error('Failed to acquire 2D canvas context');
|
||||
if (!context) throw new Error(trText('Failed to acquire 2D canvas context'));
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
@@ -233,7 +240,7 @@ async function convert() {
|
||||
const { pdf } = result;
|
||||
|
||||
if (pdf.numPages === 0) {
|
||||
throw new Error('PDF has no pages');
|
||||
throw new Error(trText('PDF has no pages'));
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
@@ -292,7 +299,7 @@ async function convert() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -320,13 +327,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (qualitySlider && qualityValue) {
|
||||
qualitySlider.addEventListener('input', () => {
|
||||
qualityValue.textContent = `${qualitySlider.value}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: qualitySlider.value,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (scaleSlider && scaleValue) {
|
||||
scaleSlider.addEventListener('input', () => {
|
||||
scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`;
|
||||
scaleValue.textContent = trText('{{value}}x', {
|
||||
value: parseFloat(scaleSlider.value).toFixed(1),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -398,7 +409,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setInputValidity(
|
||||
yearInput,
|
||||
!isNaN(year) && year >= 1900 && year <= 2100,
|
||||
'Year must be between 1900 and 2100'
|
||||
trText('Year must be between 1900 and 2100')
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -414,7 +425,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setInputValidity(
|
||||
ratingInput,
|
||||
!isNaN(rating) && rating >= 0 && rating <= 5,
|
||||
'Rating must be between 0 and 5'
|
||||
trText('Rating must be between 0 and 5')
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -429,7 +440,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setInputValidity(
|
||||
numberInput,
|
||||
!isNaN(parseFloat(val)) && parseFloat(val) >= 0,
|
||||
'Must be a positive number'
|
||||
trText('Must be a positive number')
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -445,7 +456,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setInputValidity(
|
||||
volumeInput,
|
||||
!isNaN(vol) && vol >= 1,
|
||||
'Must be a positive integer'
|
||||
trText('Must be a positive integer')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
@@ -100,7 +102,12 @@ async function convert() {
|
||||
const allRows: (string | null)[][] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
showLoader(
|
||||
trText('Scanning page {{n}} of {{pageCount}}...', {
|
||||
n: i + 1,
|
||||
pageCount,
|
||||
})
|
||||
);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
@@ -126,14 +133,17 @@ async function convert() {
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
|
||||
const message = e instanceof Error ? e.message : trText('Unknown error');
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('Failed to convert PDF to CSV. {{message}}', { message })
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createIcons, icons } from 'lucide';
|
||||
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let file: File | null = null;
|
||||
|
||||
const updateUI = () => {
|
||||
@@ -86,7 +88,12 @@ async function convert() {
|
||||
const allTables: TableData[] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||
showLoader(
|
||||
trText('Scanning page {{n}} of {{pageCount}}...', {
|
||||
n: i + 1,
|
||||
pageCount,
|
||||
})
|
||||
);
|
||||
const page = doc.getPage(i);
|
||||
const tables = page.findTables();
|
||||
|
||||
@@ -112,10 +119,10 @@ async function convert() {
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
||||
} else {
|
||||
allTables.forEach((table, idx) => {
|
||||
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(
|
||||
0,
|
||||
31
|
||||
);
|
||||
const sheetName = trText('Table {{n}} (Page {{page}})', {
|
||||
n: idx + 1,
|
||||
page: table.page,
|
||||
}).substring(0, 31);
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||
});
|
||||
@@ -128,20 +135,25 @@ async function convert() {
|
||||
downloadFile(blob, `${baseName}.xlsx`);
|
||||
showAlert(
|
||||
'Success',
|
||||
`Extracted ${allTables.length} table(s) to Excel!`,
|
||||
trText('Extracted {{length}} table(s) to Excel!', {
|
||||
length: allTables.length,
|
||||
}),
|
||||
'success',
|
||||
resetState
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
|
||||
const message = e instanceof Error ? e.message : trText('Unknown error');
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('Failed to convert PDF to Excel. {{message}}', { message })
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { applyGreyscale } from '../utils/image-effects.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -42,7 +42,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
}); // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -64,7 +67,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -159,7 +166,7 @@ async function convert() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -42,7 +42,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
}); // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -64,7 +67,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -167,7 +174,7 @@ async function renderPage(
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -185,7 +192,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (qualitySlider && qualityValue) {
|
||||
qualitySlider.addEventListener('input', () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
@@ -12,7 +12,7 @@ import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -54,7 +54,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -73,9 +76,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} pages', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdfDoc.numPages,
|
||||
});
|
||||
} catch {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
metaSpan.textContent = trText(
|
||||
'{{size}} • Could not load page count',
|
||||
{ size: formatBytes(file.size) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +124,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
|
||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
|
||||
@@ -126,7 +135,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${file.name} to Markdown.`,
|
||||
trText('Successfully converted {{name}} to Markdown.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -138,7 +149,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||
@@ -155,7 +170,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
|
||||
trText('Successfully converted {{length}} PDF(s) to Markdown.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -164,7 +181,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
@@ -15,7 +15,7 @@ import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -57,7 +57,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -76,10 +79,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} pages', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdfDoc.numPages,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
metaSpan.textContent = trText(
|
||||
'{{size}} • Could not load page count',
|
||||
{ size: formatBytes(file.size) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +173,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${originalFile.name} to ${level}.`,
|
||||
trText('Successfully converted {{name}} to {{level}}.', {
|
||||
name: originalFile.name,
|
||||
level,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -177,7 +189,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const convertedBlob = await convertFileToPdfA(file, level, (msg) =>
|
||||
@@ -201,7 +217,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
|
||||
trText('Successfully converted {{length}} PDF(s) to {{level}}.', {
|
||||
length: state.files.length,
|
||||
level,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -210,7 +229,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -42,7 +42,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
}); // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -64,7 +67,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -163,7 +170,7 @@ async function renderPage(
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -179,7 +186,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (scaleSlider && scaleValue) {
|
||||
scaleSlider.addEventListener('input', () => {
|
||||
scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`;
|
||||
scaleValue.textContent = trText('{{value}}x', {
|
||||
value: parseFloat(scaleSlider.value).toFixed(1),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import type { PyMuPDFInstance } from '@/types';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let pymupdf: PyMuPDFInstance | null = null;
|
||||
let files: File[] = [];
|
||||
|
||||
@@ -114,7 +116,12 @@ async function convert() {
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
|
||||
showLoader(
|
||||
trText('Converting page {{n}} of {{pageCount}}...', {
|
||||
n: i + 1,
|
||||
pageCount,
|
||||
})
|
||||
);
|
||||
const page = doc.getPage(i);
|
||||
const svgContent = page.toSvg();
|
||||
zip.file(`page_${i + 1}.svg`, svgContent);
|
||||
@@ -124,7 +131,7 @@ async function convert() {
|
||||
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
||||
showAlert(
|
||||
'Success',
|
||||
`Converted ${pageCount} pages to SVG!`,
|
||||
trText('Converted {{pageCount}} pages to SVG!', { pageCount }),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -135,14 +142,24 @@ async function convert() {
|
||||
|
||||
for (let f = 0; f < files.length; f++) {
|
||||
const file = files[f];
|
||||
showLoader(`Processing file ${f + 1} of ${files.length}...`);
|
||||
showLoader(
|
||||
trText('Processing file {{n}} of {{length}}...', {
|
||||
n: f + 1,
|
||||
length: files.length,
|
||||
})
|
||||
);
|
||||
const doc = await pymupdf.open(file);
|
||||
const pageCount = doc.pageCount;
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
showLoader(
|
||||
`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`
|
||||
trText('File {{n}}/{{length}}: Page {{n2}}/{{pageCount}}', {
|
||||
n: f + 1,
|
||||
length: files.length,
|
||||
n2: i + 1,
|
||||
pageCount,
|
||||
})
|
||||
);
|
||||
const page = doc.getPage(i);
|
||||
const svgContent = page.toSvg();
|
||||
@@ -160,21 +177,27 @@ async function convert() {
|
||||
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
||||
showAlert(
|
||||
'Success',
|
||||
`Converted ${files.length} files (${totalPages} pages) to SVG!`,
|
||||
trText('Converted {{length}} files ({{totalPages}} pages) to SVG!', {
|
||||
length: files.length,
|
||||
totalPages,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
|
||||
const message = e instanceof Error ? e.message : trText('Unknown error');
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('Failed to convert PDF to SVG. {{message}}', { message })
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -6,14 +6,12 @@ import type { PyMuPDFInstance } from '@/types';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let files: File[] = [];
|
||||
let pymupdf: PyMuPDFInstance | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -135,7 +133,9 @@ function updateUI() {
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
@@ -182,7 +182,9 @@ async function extractText() {
|
||||
|
||||
if (files.length === 1) {
|
||||
const file = files[0];
|
||||
showLoader(`Extracting text from ${file.name}...`);
|
||||
showLoader(
|
||||
trText('Extracting text from {{name}}...', { name: file.name })
|
||||
);
|
||||
|
||||
const fullText = await mupdf.pdfToText(file);
|
||||
|
||||
@@ -206,7 +208,11 @@ async function extractText() {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
showLoader(
|
||||
`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`
|
||||
trText('Extracting text from file {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const fullText = await mupdf.pdfToText(file);
|
||||
@@ -222,7 +228,9 @@ async function extractText() {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
`Extracted text from ${files.length} PDF files!`,
|
||||
trText('Extracted text from {{length}} PDF files!', {
|
||||
length: files.length,
|
||||
}),
|
||||
'success',
|
||||
() => {
|
||||
resetState();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import type Vips from 'wasm-vips';
|
||||
import wasmUrl from 'wasm-vips/vips.wasm?url';
|
||||
import type { TiffOptions } from '@/types';
|
||||
@@ -82,7 +82,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -103,7 +106,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -377,7 +384,7 @@ async function convert() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
import '../utils/setup-pdf-worker.js';
|
||||
|
||||
@@ -42,7 +42,10 @@ const updateUI = () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
}); // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -64,7 +67,11 @@ const updateUI = () => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
})
|
||||
.then((pdf) => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} {{value}}', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdf.numPages,
|
||||
value: pdf.numPages !== 1 ? t('common.pages') : t('common.page'),
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
@@ -167,7 +174,7 @@ async function renderPage(
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -185,7 +192,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (qualitySlider && qualityValue) {
|
||||
qualitySlider.addEventListener('input', () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
qualityValue.textContent = trText('{{value}}%', {
|
||||
value: Math.round(parseFloat(qualitySlider.value) * 100),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { t } from '../i18n/i18n';
|
||||
import { t, trText, onPageReady } from '../i18n/i18n';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
@@ -13,7 +13,7 @@ import { normalizeCmykForDocx } from '../utils/ghostscript-loader.js';
|
||||
import { batchDecryptIfNeeded } from '../utils/password-prompt.js';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
onPageReady(() => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
@@ -52,7 +52,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`;
|
||||
metaSpan.textContent = trText('{{size}} • {{loadingPageCount}}', {
|
||||
size: formatBytes(file.size),
|
||||
loadingPageCount: t('common.loadingPageCount'),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
@@ -71,9 +74,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
metaSpan.textContent = trText('{{size}} • {{numPages}} pages', {
|
||||
size: formatBytes(file.size),
|
||||
numPages: pdfDoc.numPages,
|
||||
});
|
||||
} catch {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
metaSpan.textContent = trText(
|
||||
'{{size}} • Could not load page count',
|
||||
{ size: formatBytes(file.size) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +120,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (state.files.length === 1) {
|
||||
const file = state.files[0];
|
||||
showLoader(`Converting ${file.name}...`);
|
||||
showLoader(trText('Converting {{name}}...', { name: file.name }));
|
||||
|
||||
const docxBlob = await pymupdf.pdfToDocx(
|
||||
await normalizeCmykForDocx(file)
|
||||
@@ -123,7 +132,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${file.name} to DOCX.`,
|
||||
trText('Successfully converted {{name}} to DOCX.', {
|
||||
name: file.name,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -135,7 +146,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(
|
||||
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||
trText('Converting {{n}}/{{length}}: {{name}}...', {
|
||||
n: i + 1,
|
||||
length: state.files.length,
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
const docxBlob = await pymupdf.pdfToDocx(
|
||||
@@ -158,7 +173,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
showAlert(
|
||||
'Conversion Complete',
|
||||
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
|
||||
trText('Successfully converted {{length}} PDF(s) to DOCX.', {
|
||||
length: state.files.length,
|
||||
}),
|
||||
'success',
|
||||
() => resetState()
|
||||
);
|
||||
@@ -167,7 +184,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Error',
|
||||
`An error occurred during conversion. Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
trText('An error occurred during conversion. Error: {{error}}', {
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
interface PdfToZipState {
|
||||
files: File[];
|
||||
}
|
||||
@@ -89,7 +91,13 @@ async function createZipArchive() {
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
showLoader(`Adding ${file.name} (${i + 1}/${pageState.files.length})...`);
|
||||
showLoader(
|
||||
trText('Adding {{name}} ({{n}}/{{length}})...', {
|
||||
name: file.name,
|
||||
n: i + 1,
|
||||
length: pageState.files.length,
|
||||
})
|
||||
);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const zipEntryName = deduplicateFileName(file.name, usedNames);
|
||||
zip.file(zipEntryName, arrayBuffer);
|
||||
@@ -130,7 +138,7 @@ function handleFileSelect(files: FileList | null) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
onPageReady(function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
+150
-103
@@ -3,6 +3,7 @@ import { createWorkflowEditor, updateNodeDisplay } from '@/js/workflow/editor';
|
||||
import type { WorkflowEditor } from '@/js/workflow/editor';
|
||||
import { executeWorkflow } from '@/js/workflow/engine';
|
||||
import { translateCategory, translateNodeLabel } from '@/js/workflow/i18n';
|
||||
import { onPageReady, trText } from '@/js/i18n/i18n.js';
|
||||
import type { NodeCategory } from '@/js/workflow/types';
|
||||
import {
|
||||
type BaseWorkflowNode,
|
||||
@@ -51,11 +52,7 @@ let workflowEditor: WorkflowEditor | null = null;
|
||||
let selectedNodeId: string | null = null;
|
||||
let deleteNodeHandler: EventListener | null = null;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
async function initializePage() {
|
||||
const container = document.getElementById('rete-container');
|
||||
@@ -104,12 +101,14 @@ async function initializePage() {
|
||||
|
||||
try {
|
||||
await executeWorkflow(editor, engine, area, (progress) => {
|
||||
const msg = progress.message || `Processing ${progress.nodeName}...`;
|
||||
const msg =
|
||||
progress.message ||
|
||||
trText('Processing {{nodeName}}...', { nodeName: progress.nodeName });
|
||||
if (statusText) statusText.textContent = msg;
|
||||
});
|
||||
if (statusText) statusText.textContent = 'Workflow completed';
|
||||
if (statusText) statusText.textContent = trText('Workflow completed');
|
||||
} catch (err) {
|
||||
if (statusText) statusText.textContent = 'Error during execution';
|
||||
if (statusText) statusText.textContent = trText('Error during execution');
|
||||
showAlert('Error', (err as Error).message);
|
||||
} finally {
|
||||
runBtn.disabled = false;
|
||||
@@ -121,7 +120,7 @@ async function initializePage() {
|
||||
await editor.clear();
|
||||
updateNodeCount();
|
||||
const statusText = document.getElementById('status-text');
|
||||
if (statusText) statusText.textContent = 'Ready';
|
||||
if (statusText) statusText.textContent = trText('Ready');
|
||||
document.getElementById('settings-sidebar')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
@@ -290,7 +289,11 @@ function updateNodeCount() {
|
||||
if (!workflowEditor) return;
|
||||
const count = workflowEditor.editor.getNodes().length;
|
||||
const el = document.getElementById('node-count');
|
||||
if (el) el.textContent = `${count} node${count !== 1 ? 's' : ''}`;
|
||||
if (el)
|
||||
el.textContent =
|
||||
count !== 1
|
||||
? trText('{{count}} nodes', { count })
|
||||
: trText('{{count}} node', { count });
|
||||
}
|
||||
|
||||
function showSaveTemplateModal(
|
||||
@@ -320,18 +323,22 @@ function showSaveTemplateModal(
|
||||
const doSave = () => {
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) {
|
||||
errorEl.textContent = 'Please enter a name.';
|
||||
errorEl.textContent = trText('Please enter a name.');
|
||||
errorEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
if (templateNameExists(name)) {
|
||||
errorEl.textContent = 'A template with this name already exists.';
|
||||
errorEl.textContent = trText('A template with this name already exists.');
|
||||
errorEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
saveWorkflow(editor, area, name);
|
||||
cleanup();
|
||||
showAlert('Saved', `Template "${name}" saved.`, 'success');
|
||||
showAlert(
|
||||
'Saved',
|
||||
trText('Template "{{name}}" saved.', { name }),
|
||||
'success'
|
||||
);
|
||||
};
|
||||
|
||||
const onKeydown = (e: KeyboardEvent) => {
|
||||
@@ -381,13 +388,17 @@ function showLoadTemplateModal(
|
||||
const loadBtn = document.createElement('button');
|
||||
loadBtn.className =
|
||||
'bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium px-3 py-1.5 rounded-md transition-colors flex-shrink-0';
|
||||
loadBtn.textContent = 'Load';
|
||||
loadBtn.textContent = trText('Load');
|
||||
loadBtn.addEventListener('click', async () => {
|
||||
const loaded = await loadWorkflow(editor, area, name);
|
||||
cleanup();
|
||||
if (loaded) {
|
||||
updateNodeCount();
|
||||
showAlert('Loaded', `Template "${name}" loaded.`, 'success');
|
||||
showAlert(
|
||||
'Loaded',
|
||||
trText('Template "{{name}}" loaded.', { name }),
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
showAlert('Error', 'Failed to load template.');
|
||||
}
|
||||
@@ -675,7 +686,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'block text-xs text-gray-400 mb-1';
|
||||
label.textContent = 'PDF Files';
|
||||
label.textContent = trText('PDF Files');
|
||||
fileSection.appendChild(label);
|
||||
|
||||
if (node.hasFile()) {
|
||||
@@ -688,7 +699,9 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.className =
|
||||
'w-full bg-gray-700 hover:bg-gray-600 text-white text-xs px-3 py-2 rounded-lg transition-colors';
|
||||
uploadBtn.textContent = node.hasFile() ? 'Add More Files' : 'Upload PDFs';
|
||||
uploadBtn.textContent = node.hasFile()
|
||||
? trText('Add More Files')
|
||||
: trText('Upload PDFs');
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -710,7 +723,9 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
} catch {
|
||||
showAlert(
|
||||
'Error',
|
||||
`Wrong password or failed to decrypt "${file.name}".`
|
||||
trText('Wrong password or failed to decrypt "{{name}}".', {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -734,13 +749,14 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'block text-xs text-gray-400 mb-1';
|
||||
label.textContent = 'Images';
|
||||
label.textContent = trText('Images');
|
||||
fileSection.appendChild(label);
|
||||
|
||||
const formatHint = document.createElement('p');
|
||||
formatHint.className = 'text-[10px] text-gray-500 mb-2';
|
||||
formatHint.textContent =
|
||||
'Supported: JPG, PNG, BMP, GIF, TIFF, WebP, HEIC, PSD, SVG, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2';
|
||||
formatHint.textContent = trText(
|
||||
'Supported: JPG, PNG, BMP, GIF, TIFF, WebP, HEIC, PSD, SVG, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2'
|
||||
);
|
||||
fileSection.appendChild(formatHint);
|
||||
|
||||
if (node.hasFile()) {
|
||||
@@ -754,8 +770,8 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
uploadBtn.className =
|
||||
'w-full bg-gray-700 hover:bg-gray-600 text-white text-xs px-3 py-2 rounded-lg transition-colors';
|
||||
uploadBtn.textContent = node.hasFile()
|
||||
? 'Add More Images'
|
||||
: 'Upload Images';
|
||||
? trText('Add More Images')
|
||||
: trText('Upload Images');
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -785,7 +801,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const certLabel = document.createElement('label');
|
||||
certLabel.className = 'block text-xs text-gray-400 mb-1';
|
||||
certLabel.textContent = 'Certificate (.pfx, .p12, .pem)';
|
||||
certLabel.textContent = trText('Certificate (.pfx, .p12, .pem)');
|
||||
certSection.appendChild(certLabel);
|
||||
|
||||
if (node.hasCertFile()) {
|
||||
@@ -803,7 +819,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className =
|
||||
'text-red-400 hover:text-red-300 text-xs flex-shrink-0';
|
||||
removeBtn.textContent = 'Remove';
|
||||
removeBtn.textContent = trText('Remove');
|
||||
removeBtn.addEventListener('click', () => {
|
||||
node.removeCert();
|
||||
showNodeSettings(node);
|
||||
@@ -818,7 +834,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const pwLabel = document.createElement('label');
|
||||
pwLabel.className = 'block text-xs text-gray-400 mb-1';
|
||||
pwLabel.textContent = 'Certificate Password';
|
||||
pwLabel.textContent = trText('Certificate Password');
|
||||
pwSection.appendChild(pwLabel);
|
||||
|
||||
const pwRow = document.createElement('div');
|
||||
@@ -826,14 +842,14 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const pwInput = document.createElement('input');
|
||||
pwInput.type = 'password';
|
||||
pwInput.placeholder = 'Enter password...';
|
||||
pwInput.placeholder = trText('Enter password...');
|
||||
pwInput.className =
|
||||
'flex-1 bg-gray-700 border border-gray-600 text-white text-xs px-3 py-2 rounded-lg focus:outline-none focus:border-indigo-500';
|
||||
|
||||
const unlockBtn = document.createElement('button');
|
||||
unlockBtn.className =
|
||||
'bg-indigo-600 hover:bg-indigo-500 text-white text-xs px-3 py-2 rounded-lg transition-colors flex-shrink-0';
|
||||
unlockBtn.textContent = 'Unlock';
|
||||
unlockBtn.textContent = trText('Unlock');
|
||||
|
||||
const statusMsg = document.createElement('div');
|
||||
statusMsg.className = 'text-xs mt-1 hidden';
|
||||
@@ -841,15 +857,15 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
const doUnlock = async () => {
|
||||
const pw = pwInput.value;
|
||||
if (!pw) return;
|
||||
unlockBtn.textContent = 'Unlocking...';
|
||||
unlockBtn.textContent = trText('Unlocking...');
|
||||
unlockBtn.disabled = true;
|
||||
const success = await node.unlockCert(pw);
|
||||
if (success) {
|
||||
showNodeSettings(node);
|
||||
} else {
|
||||
unlockBtn.textContent = 'Unlock';
|
||||
unlockBtn.textContent = trText('Unlock');
|
||||
unlockBtn.disabled = false;
|
||||
statusMsg.textContent = 'Incorrect password';
|
||||
statusMsg.textContent = trText('Incorrect password');
|
||||
statusMsg.className = 'text-xs mt-1 text-red-400';
|
||||
statusMsg.classList.remove('hidden');
|
||||
}
|
||||
@@ -866,7 +882,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
} else if (node.hasCert()) {
|
||||
const okMsg = document.createElement('div');
|
||||
okMsg.className = 'text-xs text-green-400 mb-2';
|
||||
okMsg.textContent = 'Certificate unlocked';
|
||||
okMsg.textContent = trText('Certificate unlocked');
|
||||
certSection.appendChild(okMsg);
|
||||
}
|
||||
}
|
||||
@@ -875,8 +891,8 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
uploadBtn.className =
|
||||
'w-full bg-gray-700 hover:bg-gray-600 text-white text-xs px-3 py-2 rounded-lg transition-colors';
|
||||
uploadBtn.textContent = node.hasCertFile()
|
||||
? 'Change Certificate'
|
||||
: 'Upload Certificate';
|
||||
? trText('Change Certificate')
|
||||
: trText('Upload Certificate');
|
||||
|
||||
const certInput = document.createElement('input');
|
||||
certInput.type = 'file';
|
||||
@@ -921,135 +937,156 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
cls: new (...args: unknown[]) => FileInputNode;
|
||||
label: string;
|
||||
accept: string;
|
||||
btnLabel: string;
|
||||
uploadLabel: string;
|
||||
addMoreLabel: string;
|
||||
hint?: string;
|
||||
}[] = [
|
||||
{
|
||||
cls: WordToPdfNode,
|
||||
label: 'Word Documents',
|
||||
label: trText('Word Documents'),
|
||||
accept: '.doc,.docx,.odt,.rtf',
|
||||
btnLabel: 'Documents',
|
||||
hint: 'Supported: DOC, DOCX, ODT, RTF',
|
||||
uploadLabel: trText('Upload Documents'),
|
||||
addMoreLabel: trText('Add More Documents'),
|
||||
hint: trText('Supported: DOC, DOCX, ODT, RTF'),
|
||||
},
|
||||
{
|
||||
cls: ExcelToPdfNode,
|
||||
label: 'Spreadsheets',
|
||||
label: trText('Spreadsheets'),
|
||||
accept: '.xlsx,.xls,.ods,.csv',
|
||||
btnLabel: 'Spreadsheets',
|
||||
hint: 'Supported: XLSX, XLS, ODS, CSV',
|
||||
uploadLabel: trText('Upload Spreadsheets'),
|
||||
addMoreLabel: trText('Add More Spreadsheets'),
|
||||
hint: trText('Supported: XLSX, XLS, ODS, CSV'),
|
||||
},
|
||||
{
|
||||
cls: PowerPointToPdfNode,
|
||||
label: 'Presentations',
|
||||
label: trText('Presentations'),
|
||||
accept: '.ppt,.pptx,.odp',
|
||||
btnLabel: 'Presentations',
|
||||
hint: 'Supported: PPT, PPTX, ODP',
|
||||
uploadLabel: trText('Upload Presentations'),
|
||||
addMoreLabel: trText('Add More Presentations'),
|
||||
hint: trText('Supported: PPT, PPTX, ODP'),
|
||||
},
|
||||
{
|
||||
cls: TextToPdfNode,
|
||||
label: 'Text Files',
|
||||
label: trText('Text Files'),
|
||||
accept: '.txt',
|
||||
btnLabel: 'Text Files',
|
||||
uploadLabel: trText('Upload Text Files'),
|
||||
addMoreLabel: trText('Add More Text Files'),
|
||||
},
|
||||
{
|
||||
cls: SvgToPdfNode,
|
||||
label: 'SVG Files',
|
||||
label: trText('SVG Files'),
|
||||
accept: '.svg',
|
||||
btnLabel: 'SVG Files',
|
||||
uploadLabel: trText('Upload SVG Files'),
|
||||
addMoreLabel: trText('Add More SVG Files'),
|
||||
},
|
||||
{
|
||||
cls: EpubToPdfNode,
|
||||
label: 'EPUB Files',
|
||||
label: trText('EPUB Files'),
|
||||
accept: '.epub',
|
||||
btnLabel: 'EPUB Files',
|
||||
uploadLabel: trText('Upload EPUB Files'),
|
||||
addMoreLabel: trText('Add More EPUB Files'),
|
||||
},
|
||||
{
|
||||
cls: EmailToPdfNode,
|
||||
label: 'Email Files',
|
||||
label: trText('Email Files'),
|
||||
accept: '.eml,.msg',
|
||||
btnLabel: 'Email Files',
|
||||
hint: 'Supported: EML, MSG',
|
||||
uploadLabel: trText('Upload Email Files'),
|
||||
addMoreLabel: trText('Add More Email Files'),
|
||||
hint: trText('Supported: EML, MSG'),
|
||||
},
|
||||
{
|
||||
cls: XpsToPdfNode,
|
||||
label: 'XPS Files',
|
||||
label: trText('XPS Files'),
|
||||
accept: '.xps,.oxps',
|
||||
btnLabel: 'XPS Files',
|
||||
hint: 'Supported: XPS, OXPS',
|
||||
uploadLabel: trText('Upload XPS Files'),
|
||||
addMoreLabel: trText('Add More XPS Files'),
|
||||
hint: trText('Supported: XPS, OXPS'),
|
||||
},
|
||||
{
|
||||
cls: MobiToPdfNode,
|
||||
label: 'MOBI Files',
|
||||
label: trText('MOBI Files'),
|
||||
accept: '.mobi',
|
||||
btnLabel: 'MOBI Files',
|
||||
uploadLabel: trText('Upload MOBI Files'),
|
||||
addMoreLabel: trText('Add More MOBI Files'),
|
||||
},
|
||||
{
|
||||
cls: Fb2ToPdfNode,
|
||||
label: 'FB2 Files',
|
||||
label: trText('FB2 Files'),
|
||||
accept: '.fb2',
|
||||
btnLabel: 'FB2 Files',
|
||||
uploadLabel: trText('Upload FB2 Files'),
|
||||
addMoreLabel: trText('Add More FB2 Files'),
|
||||
},
|
||||
{
|
||||
cls: CbzToPdfNode,
|
||||
label: 'Comic Archives',
|
||||
label: trText('Comic Archives'),
|
||||
accept: '.cbz,.cbr',
|
||||
btnLabel: 'Comics',
|
||||
hint: 'Supported: CBZ, CBR',
|
||||
uploadLabel: trText('Upload Comics'),
|
||||
addMoreLabel: trText('Add More Comics'),
|
||||
hint: trText('Supported: CBZ, CBR'),
|
||||
},
|
||||
{
|
||||
cls: MarkdownToPdfNode,
|
||||
label: 'Markdown Files',
|
||||
label: trText('Markdown Files'),
|
||||
accept: '.md,.markdown',
|
||||
btnLabel: 'Markdown Files',
|
||||
uploadLabel: trText('Upload Markdown Files'),
|
||||
addMoreLabel: trText('Add More Markdown Files'),
|
||||
},
|
||||
{
|
||||
cls: JsonToPdfNode,
|
||||
label: 'JSON Files',
|
||||
label: trText('JSON Files'),
|
||||
accept: '.json',
|
||||
btnLabel: 'JSON Files',
|
||||
uploadLabel: trText('Upload JSON Files'),
|
||||
addMoreLabel: trText('Add More JSON Files'),
|
||||
},
|
||||
{
|
||||
cls: XmlToPdfNode,
|
||||
label: 'XML Files',
|
||||
label: trText('XML Files'),
|
||||
accept: '.xml',
|
||||
btnLabel: 'XML Files',
|
||||
uploadLabel: trText('Upload XML Files'),
|
||||
addMoreLabel: trText('Add More XML Files'),
|
||||
},
|
||||
{
|
||||
cls: WpdToPdfNode,
|
||||
label: 'WordPerfect Files',
|
||||
label: trText('WordPerfect Files'),
|
||||
accept: '.wpd',
|
||||
btnLabel: 'WPD Files',
|
||||
uploadLabel: trText('Upload WPD Files'),
|
||||
addMoreLabel: trText('Add More WPD Files'),
|
||||
},
|
||||
{
|
||||
cls: WpsToPdfNode,
|
||||
label: 'WPS Files',
|
||||
label: trText('WPS Files'),
|
||||
accept: '.wps',
|
||||
btnLabel: 'WPS Files',
|
||||
uploadLabel: trText('Upload WPS Files'),
|
||||
addMoreLabel: trText('Add More WPS Files'),
|
||||
},
|
||||
{
|
||||
cls: PagesToPdfNode,
|
||||
label: 'Pages Files',
|
||||
label: trText('Pages Files'),
|
||||
accept: '.pages',
|
||||
btnLabel: 'Pages Files',
|
||||
uploadLabel: trText('Upload Pages Files'),
|
||||
addMoreLabel: trText('Add More Pages Files'),
|
||||
},
|
||||
{
|
||||
cls: OdgToPdfNode,
|
||||
label: 'ODG Files',
|
||||
label: trText('ODG Files'),
|
||||
accept: '.odg',
|
||||
btnLabel: 'ODG Files',
|
||||
uploadLabel: trText('Upload ODG Files'),
|
||||
addMoreLabel: trText('Add More ODG Files'),
|
||||
},
|
||||
{
|
||||
cls: PubToPdfNode,
|
||||
label: 'Publisher Files',
|
||||
label: trText('Publisher Files'),
|
||||
accept: '.pub',
|
||||
btnLabel: 'PUB Files',
|
||||
uploadLabel: trText('Upload PUB Files'),
|
||||
addMoreLabel: trText('Add More PUB Files'),
|
||||
},
|
||||
{
|
||||
cls: VsdToPdfNode,
|
||||
label: 'Visio Files',
|
||||
label: trText('Visio Files'),
|
||||
accept: '.vsd,.vsdx',
|
||||
btnLabel: 'Visio Files',
|
||||
hint: 'Supported: VSD, VSDX',
|
||||
uploadLabel: trText('Upload Visio Files'),
|
||||
addMoreLabel: trText('Add More Visio Files'),
|
||||
hint: trText('Supported: VSD, VSDX'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1081,8 +1118,8 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
uploadBtn.className =
|
||||
'w-full bg-gray-700 hover:bg-gray-600 text-white text-xs px-3 py-2 rounded-lg transition-colors';
|
||||
uploadBtn.textContent = fileNode.hasFile()
|
||||
? `Add More ${fileInputConfig.btnLabel}`
|
||||
: `Upload ${fileInputConfig.btnLabel}`;
|
||||
? fileInputConfig.addMoreLabel
|
||||
: fileInputConfig.uploadLabel;
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
@@ -1096,7 +1133,12 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
await fileNode.addFiles(files);
|
||||
showNodeSettings(node);
|
||||
} catch (err) {
|
||||
showAlert('Error', `Failed to load files: ${(err as Error).message}`);
|
||||
showAlert(
|
||||
'Error',
|
||||
trText('Failed to load files: {{error}}', {
|
||||
error: (err as Error).message,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1119,7 +1161,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
if (controlEntries.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'text-xs text-gray-500';
|
||||
empty.textContent = 'No configurable settings for this node.';
|
||||
empty.textContent = trText('No configurable settings for this node.');
|
||||
content.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
@@ -1128,16 +1170,18 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const controlHints: Record<string, string> = {
|
||||
pages: 'e.g. 1-3, 5, 7-9',
|
||||
whitelist: 'Limit recognized characters (leave empty for all)',
|
||||
afterPage: 'Insert blank pages after this page number',
|
||||
x0: 'Left edge in points (1 inch = 72 pts)',
|
||||
y0: 'Top edge in points',
|
||||
x1: 'Right edge in points',
|
||||
y1: 'Bottom edge in points',
|
||||
targetKilobytes:
|
||||
'Finds the best quality under this size. 0 means no target.',
|
||||
retainPageLabels:
|
||||
"Off (default): natural 1–N numbering. On: each file's original page labels are preserved (may produce duplicate labels).",
|
||||
whitelist: trText('Limit recognized characters (leave empty for all)'),
|
||||
afterPage: trText('Insert blank pages after this page number'),
|
||||
x0: trText('Left edge in points (1 inch = 72 pts)'),
|
||||
y0: trText('Top edge in points'),
|
||||
x1: trText('Right edge in points'),
|
||||
y1: trText('Bottom edge in points'),
|
||||
targetKilobytes: trText(
|
||||
'Finds the best quality under this size. 0 means no target.'
|
||||
),
|
||||
retainPageLabels: trText(
|
||||
"Off (default): natural 1–N numbering. On: each file's original page labels are preserved (may produce duplicate labels)."
|
||||
),
|
||||
};
|
||||
|
||||
const inputClass =
|
||||
@@ -1166,7 +1210,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const controlLabel = document.createElement('label');
|
||||
controlLabel.className = 'block text-xs text-gray-400 mb-1';
|
||||
controlLabel.textContent = formatLabel(key);
|
||||
controlLabel.textContent = trText(formatLabel(key));
|
||||
wrapper.appendChild(controlLabel);
|
||||
|
||||
if (spec?.kind === 'multiselect') {
|
||||
@@ -1179,7 +1223,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.placeholder = 'Search languages...';
|
||||
searchInput.placeholder = trText('Search languages...');
|
||||
searchInput.className = inputClass;
|
||||
container.appendChild(searchInput);
|
||||
|
||||
@@ -1200,7 +1244,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
const tag = document.createElement('span');
|
||||
tag.className =
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded bg-indigo-600 text-white text-[10px]';
|
||||
tag.textContent = opt.label;
|
||||
tag.textContent = trText(opt.label);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.textContent = '\u00d7';
|
||||
@@ -1226,7 +1270,10 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
listDiv.innerHTML = '';
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
for (const opt of options) {
|
||||
if (lowerFilter && !opt.label.toLowerCase().includes(lowerFilter))
|
||||
if (
|
||||
lowerFilter &&
|
||||
!trText(opt.label).toLowerCase().includes(lowerFilter)
|
||||
)
|
||||
continue;
|
||||
const label = document.createElement('label');
|
||||
label.className =
|
||||
@@ -1246,7 +1293,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
updateCtrl();
|
||||
});
|
||||
label.appendChild(cb);
|
||||
label.appendChild(document.createTextNode(opt.label));
|
||||
label.appendChild(document.createTextNode(trText(opt.label)));
|
||||
listDiv.appendChild(label);
|
||||
}
|
||||
}
|
||||
@@ -1264,7 +1311,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
for (const opt of spec.options) {
|
||||
const option = document.createElement('option');
|
||||
option.value = opt.value;
|
||||
option.textContent = opt.label;
|
||||
option.textContent = trText(opt.label);
|
||||
if (currentValue === opt.value) option.selected = true;
|
||||
select.appendChild(option);
|
||||
}
|
||||
@@ -1397,7 +1444,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
|
||||
summary.className =
|
||||
'text-xs font-medium text-gray-400 cursor-pointer select-none flex items-center justify-between';
|
||||
const summaryText = document.createElement('span');
|
||||
summaryText.textContent = 'Advanced Settings';
|
||||
summaryText.textContent = trText('Advanced Settings');
|
||||
summary.appendChild(summaryText);
|
||||
const chevron = document.createElement('i');
|
||||
chevron.className =
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
compressImageBytes,
|
||||
} from '../utils/image-compress.js';
|
||||
|
||||
import { trText, onPageReady } from '../i18n/i18n.js';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
onPageReady(initializePage);
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
@@ -137,7 +135,9 @@ function updateUI() {
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
sizeSpan.textContent = trText('({{size}})', {
|
||||
size: formatBytes(file.size),
|
||||
});
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
@@ -180,7 +180,9 @@ function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) {
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
return reject(
|
||||
new Error(trText('Canvas toBlob conversion failed.'))
|
||||
);
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
@@ -195,7 +197,9 @@ function sanitizeImageAsJpeg(imageBytes: Uint8Array | ArrayBuffer) {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
trText(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
Loaded 100 of 3344 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user